mirror of
https://github.com/apernet/hysteria.git
synced 2025-07-05 01:27:01 +00:00
Compare commits
925 commits
Author | SHA1 | Date | |
---|---|---|---|
|
88890dde2d | ||
|
b5ddcb5bc4 | ||
|
483fde51b1 | ||
|
3a9e952af0 | ||
|
2adeec2900 | ||
|
aa5f68a6f7 | ||
|
b2567df63c | ||
|
16c7ebeeb6 | ||
|
c2c4a9545e | ||
|
29cd04fdef | ||
|
5239a23aee | ||
|
245c6e9bd1 | ||
|
ffab01730a | ||
|
401ed5245d | ||
|
9466bc4e2f | ||
|
e11ad2b93b | ||
|
7652ddcd99 | ||
|
e1df8aa4e2 | ||
|
8c05217590 | ||
|
d86aa0b4e2 | ||
|
537e8144ea | ||
|
817d6c9a2d | ||
|
5520bcc405 | ||
|
9e90d7d155 | ||
|
8aa80c233e | ||
|
2bdaf7b46a | ||
|
53a4ce2598 | ||
|
cd396eea60 | ||
|
400fed3bd6 | ||
|
6655d2a78d | ||
|
5e11ea18fb | ||
|
d8c61c59d7 | ||
|
16c964b3e1 | ||
|
15e31d48a0 | ||
|
3e8c20518d | ||
|
9cb8cb4f53 | ||
|
7ac8d87dda | ||
|
0681638568 | ||
|
c34f23755a | ||
|
a52b02ba2b | ||
|
d4a1c2b580 | ||
|
685cd3663b | ||
|
04cf6f2e1a | ||
|
a2c7b8fd19 | ||
|
9a21e2e8c6 | ||
|
a9422e63be | ||
|
d65997c02b | ||
|
78598bfd1b | ||
|
4567713ed8 | ||
|
99e959f8c9 | ||
|
af2d75d1d0 | ||
|
b960beabbd | ||
|
ecc95fb973 | ||
|
1001b2b1ad | ||
|
ef6da94927 | ||
|
b3116c6268 | ||
|
947701897b | ||
|
4e2f138008 | ||
|
dc023ae13a | ||
|
931fc2fdb2 | ||
|
4ecbd57294 | ||
|
21ea2a024a | ||
|
d4b9c5a822 | ||
|
4ed3f21d72 | ||
|
667b08ec3e | ||
|
bcf830c29a | ||
|
45893b5d1e | ||
|
57a48a674b | ||
|
fd2d20a46a | ||
|
903666f18e | ||
|
00df1cab0f | ||
|
4c04660684 | ||
|
f2712aac93 | ||
|
55c3a064cc | ||
|
7e70547dbd | ||
|
f014c00546 | ||
|
48bf9b964a | ||
|
442ee3898c | ||
|
d527ff13b5 | ||
|
604132f8d0 | ||
|
c62c8c5513 | ||
|
b563f3981f | ||
|
a7ecd08046 | ||
|
458ee1386c | ||
|
8d9c7fa04c | ||
|
0ce3df4396 | ||
|
5315b60610 | ||
|
6a90fe18ee | ||
|
deeeafd8d7 | ||
|
b481b49a28 | ||
|
7b4def4c35 | ||
|
3412368d20 | ||
|
16bfdc7720 | ||
|
8aab735029 | ||
|
988b86ae55 | ||
|
c78dbb38a1 | ||
|
2c62a1a1b4 | ||
|
506d8e01b8 | ||
|
c5e7aa3f02 | ||
|
a852febc1f | ||
|
feacb1f85e | ||
|
4c2a905892 | ||
|
d318903783 | ||
|
18d075cc07 | ||
|
bc0e18980b | ||
|
52c8f82c2b | ||
|
23b79688fb | ||
|
e1ac7c88ab | ||
|
492145c124 | ||
|
8fca92a319 | ||
|
10234e5daf | ||
|
3c22e5967f | ||
|
3024fc079c | ||
|
146d077124 | ||
|
9e9b4dbc7d | ||
|
788d04cfdd | ||
|
12d4fbae80 | ||
|
44f4ddacfe | ||
|
adee547c21 | ||
|
09b08fa494 | ||
|
cd512ce1c6 | ||
|
5b0ab76d44 | ||
|
396dd0a68c | ||
|
e0e75c4630 | ||
|
1742f83b8e | ||
|
0c198abd2e | ||
|
15e58468a7 | ||
|
b216c4f128 | ||
|
4c0bd74094 | ||
|
2701a6e23f | ||
|
a3c4cfa4b5 | ||
|
9d4b3e608a | ||
|
6a34a9e7a0 | ||
|
ba9b3cdebb | ||
|
88eef7617f | ||
|
2366882bd6 | ||
|
415ef42b5a | ||
|
c831b987cd | ||
|
b79c43171a | ||
|
d2805577ff | ||
|
8412ec3ab3 | ||
|
59f16d0792 | ||
|
00813c4622 | ||
|
b8b8122ecf | ||
|
e7d7dbbf8f | ||
|
f586d513bc | ||
|
c392b0338b | ||
|
3409904294 | ||
|
1b78b2ec90 | ||
|
bf1cc0847e | ||
|
dc1f58414a | ||
|
2fcbde08d8 | ||
|
9752347073 | ||
|
2408301c98 | ||
|
234dc4508b | ||
|
6e00aa3114 | ||
|
a656a2042d | ||
|
e1d8901c16 | ||
|
8e886b6e05 | ||
|
044620a5db | ||
|
6d9c4fd4e5 | ||
|
8d9b10a259 | ||
|
34574e0339 | ||
|
d9346f6c24 | ||
|
44b36f56ac | ||
|
6b5486fc09 | ||
|
e6da1f348c | ||
|
5bebfd5732 | ||
|
297d64e48f | ||
|
e1d7ce4640 | ||
|
9520d84094 | ||
|
13586df2ba | ||
|
65f5e9caa5 | ||
|
3e34da1aa8 | ||
|
a05383c2a1 | ||
|
03c8b5e6b9 | ||
|
f91efbeded | ||
|
3de65357d4 | ||
|
0f388396a4 | ||
|
2cb0662075 | ||
|
d34ff757c3 | ||
|
de7d7dc51e | ||
|
02fa2cde0a | ||
|
2d4dd66c0e | ||
|
7aa0becd84 | ||
|
bbf4231091 | ||
|
89a99a08bf | ||
|
a037880f88 | ||
|
2d7d67bf27 | ||
|
5eb04bb46d | ||
|
9dfb5808e0 | ||
|
ddb5b511fc | ||
|
bdd4114654 | ||
|
6374ea11c4 | ||
|
aab104ae2e | ||
|
dc8fe45a1a | ||
|
87bbf17bc5 | ||
|
b287020daa | ||
|
2e93c12cdc | ||
|
91406ab0f9 | ||
|
92ed8f5e6a | ||
|
38d9248acd | ||
|
0cde4f405f | ||
|
4aec8166b3 | ||
|
f10805dc13 | ||
|
804e3f6df9 | ||
|
57e6e47f19 | ||
|
5c423d16fe | ||
|
45593c02fc | ||
|
caf6c66599 | ||
|
1f05791a4e | ||
|
55beaff012 | ||
|
b07b12a651 | ||
|
b5c1980202 | ||
|
15b94d5c40 | ||
|
9a80fe589a | ||
|
fda93579f0 | ||
|
8b46cc08f0 | ||
|
9349f0a1a3 | ||
|
2780dc2766 | ||
|
16ec4550c3 | ||
|
3216814440 | ||
|
ee056deaad | ||
|
78aa85d35c | ||
|
9c51995cc4 | ||
|
02baab148a | ||
|
d82d76743f | ||
|
e99ac076da | ||
|
a0bd58063b | ||
|
84d72ef0b3 | ||
|
0c2b0234fa | ||
|
982be5498b | ||
|
1ac9d4956b | ||
|
ea66299d0f | ||
|
a531542723 | ||
|
842b0ab3f7 | ||
|
6dea0adb19 | ||
|
e22aa0630b | ||
|
f0d59ebee1 | ||
|
bb99579bb9 | ||
|
80bc3b3a44 | ||
|
ae402d9d91 | ||
|
84b54eb702 | ||
|
e648321b96 | ||
|
c4993f8dd1 | ||
|
f0c7af50a5 | ||
|
e5ef67ecf9 | ||
|
f3d675145f | ||
|
b7dff17fd3 | ||
|
4a502b4b5d | ||
|
8969bbe25c | ||
|
d73edff71e | ||
|
800ed73069 | ||
|
6cfef8ce73 | ||
|
405572dc6e | ||
|
03a76b2746 | ||
|
a412af48b9 | ||
|
8f787b4b73 | ||
|
21cd348c8b | ||
|
bb3b83f4de | ||
|
9476976950 | ||
|
e70838cd98 | ||
|
f48a5edd39 | ||
|
c341aea5d0 | ||
|
4cf253efec | ||
|
3a77d4756e | ||
|
faeef50fc0 | ||
|
ee3a23fb3e | ||
|
6d6a26b399 | ||
|
0a77ce4d64 | ||
|
cccb9558c0 | ||
|
e052f767db | ||
|
c62dc51017 | ||
|
9940ea9dd7 | ||
|
0305037694 | ||
|
6872bb0263 | ||
|
cb8e6eeb93 | ||
|
a1bd044467 | ||
|
7b68bbf84a | ||
|
14e3211226 | ||
|
a4a2f662bf | ||
|
9ff8020803 | ||
|
a633d3e320 | ||
|
e6cb3df546 | ||
|
b2d4bac556 | ||
|
affe092336 | ||
|
fcc3dd4988 | ||
|
e604c12f7e | ||
|
bcacc46f1d | ||
|
ef6a231787 | ||
|
ee6ae941f4 | ||
|
c72884f30c | ||
|
13c63cdfaf | ||
|
dfa95811e8 | ||
|
f854c38870 | ||
|
131306b72b | ||
|
d513ae115b | ||
|
e57eeb986b | ||
|
a6da40df11 | ||
|
6b5c791416 | ||
|
ca53344fed | ||
|
61a68a18b9 | ||
|
594fde1ff8 | ||
|
197e913dce | ||
|
4ebc765f43 | ||
|
994cef32ea | ||
|
7c46e845a6 | ||
|
5597b482a9 | ||
|
89429598bf | ||
|
282ec2a0c5 | ||
|
86c8b3845f | ||
|
bd03e59a77 | ||
|
f8482a3ddb | ||
|
6f1807a376 | ||
|
7ba9fb266f | ||
|
922edce1d0 | ||
|
39518268f0 | ||
|
844e94d6ca | ||
|
8a065b1368 | ||
|
8faaf3b2e8 | ||
|
fd6bef4c7e | ||
|
63dd6e83d8 | ||
|
d484849882 | ||
|
7135f04fa2 | ||
|
1173252f62 | ||
|
1d0560dd34 | ||
|
62f7bb9160 | ||
|
d109d882f6 | ||
|
7972121686 | ||
|
6425098215 | ||
|
00841504b7 | ||
|
31dca9476d | ||
|
1965535e69 | ||
|
056c46f4d0 | ||
|
c73570f582 | ||
|
5a7dfd8a3b | ||
|
a28234a21a | ||
|
7ae977866a | ||
|
1ab983e61f | ||
|
3ccb0a9ac5 | ||
|
55bf6a6d71 | ||
|
a13b303f65 | ||
|
f6fecba6c7 | ||
|
9e9a820b68 | ||
|
7580c0c641 | ||
|
d4afd2f474 | ||
|
5c72b383ce | ||
|
0fe42d41d6 | ||
|
6d4a843fd2 | ||
|
9b8f914eab | ||
|
83a6e5f9a9 | ||
|
1736e6026e | ||
|
1f1b071ca8 | ||
|
1abddbff19 | ||
|
e177b3563f | ||
|
7fe0139016 | ||
|
a8640d312f | ||
|
296e2ecd23 | ||
|
a2347ad75f | ||
|
764bb081af | ||
|
6de749b1f1 | ||
|
5379655798 | ||
|
ec91aa592a | ||
|
f19bf08c35 | ||
|
bd2d4c9b8e | ||
|
3eeab8a349 | ||
|
099faa93a4 | ||
|
08372d972c | ||
|
4aafb5e2da | ||
|
dcecc25537 | ||
|
9136a1db19 | ||
|
c7545cc870 | ||
|
9d361555ef | ||
|
e11762a196 | ||
|
f9bab4657c | ||
|
299791d7e5 | ||
|
fc276ea826 | ||
|
dadf6bf0ab | ||
|
dc7d62f6f8 | ||
|
d6c5ef2a0a | ||
|
45b71f2386 | ||
|
4cc365d9d2 | ||
|
ee70476030 | ||
|
6bcb00a0cc | ||
|
81a98e1e5a | ||
|
5efd0f8f44 | ||
|
3e5eccd6e3 | ||
|
1ea7c515ae | ||
|
26fdba6049 | ||
|
353aacfd62 | ||
|
09355c4e21 | ||
|
332d2ea32d | ||
|
3c3c2a51a8 | ||
|
b12bd74ac7 | ||
|
e602ec6169 | ||
|
a47285896a | ||
|
acfb10efc0 | ||
|
c27e6fb8d9 | ||
|
f5183ca564 | ||
|
a7d74a9ec1 | ||
|
6fa958815b | ||
|
cd2524c767 | ||
|
cab753718d | ||
|
b64f0a764c | ||
|
2749f7262d | ||
|
25b8eef959 | ||
|
d3db1e4a1d | ||
|
cbfb1998a5 | ||
|
ceb3c7f6a8 | ||
|
4060bcb806 | ||
|
449d98ac47 | ||
|
cc0d0181e1 | ||
|
f95a31120d | ||
|
7307eea2a8 | ||
|
601ad6b61c | ||
|
2c7db03243 | ||
|
531b23baa4 | ||
|
3b4af8035b | ||
|
45c3fc54bd | ||
|
02fca02ddc | ||
|
13debaa0ad | ||
|
6ad44d183e | ||
|
7c94b072ed | ||
|
be76f0650e | ||
|
d10398a11f | ||
|
723612c297 | ||
|
c64b36b8f4 | ||
|
b62ef83206 | ||
|
a59111faad | ||
|
62fddff137 | ||
|
e381c2eae8 | ||
|
d4e3833641 | ||
|
fd4d095dcd | ||
|
fb7e6ed915 | ||
|
ddc7fa8456 | ||
|
7a3c23032b | ||
|
1629f0fc8e | ||
|
37385f623f | ||
|
6172f2ac53 | ||
|
dd836b4496 | ||
|
55fb903192 | ||
|
1c7cb23389 | ||
|
e48bb98024 | ||
|
27460960ab | ||
|
f0ad2f77ca | ||
|
cbedb27f0f | ||
|
f142a24047 | ||
|
1f499f07c7 | ||
|
a2fbcc6507 | ||
|
6245f83262 | ||
|
b25fb63d5b | ||
|
20a57e180d | ||
|
07b7f14bef | ||
|
0dbd6af683 | ||
|
6aa60e12d1 | ||
|
4b2140f589 | ||
|
e21e5c67a8 | ||
|
4c24edaac1 | ||
|
baee5689c1 | ||
|
1b3a718ac8 | ||
|
458382dd3d | ||
|
7e177a22f7 | ||
|
8ca414e548 | ||
|
e97a81a8a9 | ||
|
eb7e91e5ce | ||
|
8342827339 | ||
|
16a2294cd1 | ||
|
cc8a889503 | ||
|
1317c599b8 | ||
|
bacc8ff5ed | ||
|
1209aa9e11 | ||
|
b27628607a | ||
|
fefabaf739 | ||
|
fcb8965987 | ||
|
4334d8afb8 | ||
|
5b54edd09a | ||
|
901e0480f2 | ||
|
ea29efc298 | ||
|
635ad9782a | ||
|
cefe5d9f76 | ||
|
393e0d00f2 | ||
|
5586303825 | ||
|
41f10a22c4 | ||
|
ebb9b3217e | ||
|
9f54aade8f | ||
|
c0ab06e961 | ||
|
bab6089f0e | ||
|
355a5949e2 | ||
|
8f310fb798 | ||
|
cbc29ea4e5 | ||
|
12b27ea1b1 | ||
|
17c35f11f8 | ||
|
ca07cf6f18 | ||
|
e6b0f0b76d | ||
|
b94f8a1eaf | ||
|
779e962d49 | ||
|
1b3b038728 | ||
|
fb2a0da88f | ||
|
12cff70aac | ||
|
8c99fe3754 | ||
|
13d46da998 | ||
|
20898f2990 | ||
|
604d4fc652 | ||
|
1d9fa029c2 | ||
|
23f1546591 | ||
|
a48d6ddb7c | ||
|
30e17a40c0 | ||
|
810bfd7022 | ||
|
b4dfbaa3f2 | ||
|
a7c159eeb3 | ||
|
da574f6654 | ||
|
cb4daac18d | ||
|
2f1b266a52 | ||
|
8a1f4e4c04 | ||
|
f0cfbb2653 | ||
|
3e1d2a8c92 | ||
|
517c5a4bc4 | ||
|
02e987e639 | ||
|
f7dffd027f | ||
|
29459d768d | ||
|
a5647379b1 | ||
|
042f42b655 | ||
|
568c905344 | ||
|
6e3a3f5388 | ||
|
f203ab901e | ||
|
dd4c17972f | ||
|
7430f37b21 | ||
|
14043d9b50 | ||
|
9cec566384 | ||
|
f90dd47811 | ||
|
4c8871070c | ||
|
432a29ff9a | ||
|
455f36734c | ||
|
e832b4896e | ||
|
a95dcf0e39 | ||
|
e6cb3507ca | ||
|
01c3eef825 | ||
|
067f3b5c6c | ||
|
cceba2ab79 | ||
|
c29eba1416 | ||
|
392cde1120 | ||
|
fc28c01980 | ||
|
fcc2f06bc1 | ||
|
986c163040 | ||
|
dbe9f3414c | ||
|
608810ceac | ||
|
173d3a5a1d | ||
|
e2cc0dff48 | ||
|
960a380521 | ||
|
249af28506 | ||
|
2e6cb1c838 | ||
|
7a7fda67f2 | ||
|
2d8425e736 | ||
|
59b15f8313 | ||
|
7163b8ce9b | ||
|
80779d76c0 | ||
|
73df9973d0 | ||
|
ed14540d26 | ||
|
3184c42956 | ||
|
e338ed60cb | ||
|
745d84e3c7 | ||
|
80faa4aaf8 | ||
|
536fa24595 | ||
|
7481a859b6 | ||
|
9eb0ea4367 | ||
|
26d557a6c4 | ||
|
bef694727d | ||
|
423c551306 | ||
|
21f4fa7d86 | ||
|
6c6a804736 | ||
|
00ec7e5ad9 | ||
|
cdbfa0fca2 | ||
|
a2bc061e74 | ||
|
b386bfbeef | ||
|
c7a87b2c43 | ||
|
589f4a8256 | ||
|
7ce4bf7dbe | ||
|
4a0b66e9b3 | ||
|
fa7b90b041 | ||
|
466cc35d90 | ||
|
18b46566ca | ||
|
4a272ba704 | ||
|
099750df05 | ||
|
55cdd20345 | ||
|
c2bc6a224d | ||
|
385c2d6845 | ||
|
9ece6c89e9 | ||
|
31a569cd66 | ||
|
052380d65a | ||
|
a5bca3bf38 | ||
|
00d1520e44 | ||
|
20de0ca335 | ||
|
fbe0cf4156 | ||
|
0cf7d90f7b | ||
|
ff399e98fe | ||
|
d72866d61e | ||
|
325869bdb5 | ||
|
2e80334841 | ||
|
263ac8d313 | ||
|
508ff69ea9 | ||
|
a7675b2f5b | ||
|
e329e8a2e9 | ||
|
0ea0978906 | ||
|
a5985c5b6f | ||
|
0119024392 | ||
|
6ac5e0e455 | ||
|
1c7880e2f6 | ||
|
10282e0ffd | ||
|
4a08e5226b | ||
|
904a197af9 | ||
|
7cc5e9e41a | ||
|
17ba89ea03 | ||
|
4f14601896 | ||
|
555c71e61a | ||
|
2e84ca6ebe | ||
|
a3f53e9761 | ||
|
f0f900c2dc | ||
|
a4da230517 | ||
|
21b2830289 | ||
|
72b9ddcfa4 | ||
|
fbcfc7dfc2 | ||
|
e187be50de | ||
|
ee1fd45030 | ||
|
0e55506002 | ||
|
58912ce169 | ||
|
8b0a157e0b | ||
|
3d54cb43af | ||
|
f92c2cdda7 | ||
|
919fbb7152 | ||
|
b247919a03 | ||
|
d9d80ecbb1 | ||
|
9e521a7615 | ||
|
cf970f09e4 | ||
|
83764ba9de | ||
|
ce86fd918a | ||
|
c98c7eca4e | ||
|
b1d9ab6c6a | ||
|
223a9a4203 | ||
|
e3c3088596 | ||
|
ca3de154ba | ||
|
7126425499 | ||
|
c018eb11a9 | ||
|
342a9be127 | ||
|
7754ed0371 | ||
|
a681283921 | ||
|
b2b47022b5 | ||
|
8b93cf5f08 | ||
|
2bef487bbb | ||
|
9ad19fdc9f | ||
|
bda076694a | ||
|
eb966ec162 | ||
|
35c9a76230 | ||
|
3c80671405 | ||
|
f9f668644b | ||
|
57590900ac | ||
|
4b2fd55060 | ||
|
870cf6ed01 | ||
|
42d58c97e6 | ||
|
ad2d46259e | ||
|
ab845d9a8c | ||
|
e67f2d6c7e | ||
|
2d5ed53d8c | ||
|
03e397b152 | ||
|
1d756528a8 | ||
|
f476ad3f67 | ||
|
0f1108bd48 | ||
|
249fb061d4 | ||
|
4d6bfe8fa8 | ||
|
29ad5d8781 | ||
|
26b5c2c9ee | ||
|
f22705be2c | ||
|
4cd18e6685 | ||
|
f7de18fd43 | ||
|
2fb70bdb58 | ||
|
7c9fbf22dd | ||
|
a31f826a1e | ||
|
a10abd473f | ||
|
8960aefca3 | ||
|
ac8a27df6e | ||
|
aad4c44b3d | ||
|
944c0ecf64 | ||
|
88099ee72d | ||
|
1462cadfdc | ||
|
4747be198e | ||
|
1b2eb49da1 | ||
|
fbfd933fac | ||
|
ab2ad4aa6d | ||
|
ad095e4545 | ||
|
12c20211b0 | ||
|
e5e45720bd | ||
|
8f5ec1aac2 | ||
|
cdc874a608 | ||
|
634eb26a4b | ||
|
943603eb20 | ||
|
bd9f3e5846 | ||
|
45eacaf886 | ||
|
b3608956be | ||
|
0ba4c36bc6 | ||
|
0bd4574988 | ||
|
4947af7fa8 | ||
|
720b97da67 | ||
|
80b0c87654 | ||
|
4abb30620a | ||
|
a4c61e285e | ||
|
f3f604b59a | ||
|
a5b7bc7707 | ||
|
af2e0e9676 | ||
|
562d8402f5 | ||
|
3e63cede8a | ||
|
3f0953ffdd | ||
|
a2ab9e0425 | ||
|
21382fe39c | ||
|
345836abc4 | ||
|
b84b4b77af | ||
|
e0d7a297a6 | ||
|
b9d0108af3 | ||
|
7051e229d8 | ||
|
056643f343 | ||
|
9fb43fbc43 | ||
|
a83494da78 | ||
|
9b94d80baa | ||
|
9f40653191 | ||
|
87676d41bd | ||
|
53cd363955 | ||
|
390a7fe556 | ||
|
1931fe76d8 | ||
|
a6a33e9145 | ||
|
aa55669be8 | ||
|
9597f7b31d | ||
|
3d746075ab | ||
|
160145f712 | ||
|
5f3fb2dc2d | ||
|
4a3e996c9a | ||
|
03b562713b | ||
|
e7e762c022 | ||
|
3762b07428 | ||
|
879fb04b1a | ||
|
da16c8862d | ||
|
c8eaa2bce4 | ||
|
7dd2d582b4 | ||
|
5f9b1bffb9 | ||
|
595d1bda8a | ||
|
5e4773a117 | ||
|
8a64099a96 | ||
|
d1e7d4c7cb | ||
|
5aa96cdc93 | ||
|
575de280ff | ||
|
6d8e79170e | ||
|
3cd20f6cdb | ||
|
6d7f7016fb | ||
|
d8b782c984 | ||
|
1a1c69e6ad | ||
|
32f35894cc | ||
|
31d34e5269 | ||
|
119f2a8bcb | ||
|
eb7de74355 | ||
|
74276ddab1 | ||
|
b36a4d0cd4 | ||
|
8c7f063a87 | ||
|
4dee70c832 | ||
|
e0a237f720 | ||
|
57d4e58ae2 | ||
|
ee4b8e6483 | ||
|
ecf6b8b706 | ||
|
f3b999bce0 | ||
|
6d628e1e8c | ||
|
343bfc3e0a | ||
|
e9974b0398 | ||
|
7a0977023e | ||
|
28b6210ce5 | ||
|
6e42cb4586 | ||
|
95d8ea49cc | ||
|
77a0063621 | ||
|
029eee25bf | ||
|
e0fe316326 | ||
|
ab5eded6d6 | ||
|
75b640f19c | ||
|
65fcdd51bc | ||
|
5d98f7f5c3 | ||
|
56825c5525 | ||
|
3dce74cb0f | ||
|
5b90ac21fa | ||
|
63c93c1f3a | ||
|
c53f9ecf5b | ||
|
b363e9d0ba | ||
|
f343546c49 | ||
|
d28d534092 | ||
|
9dedd533d6 | ||
|
8a56ed5795 | ||
|
6366e54001 | ||
|
94a4c2d570 | ||
|
ba47130990 | ||
|
877affec42 | ||
|
a9ad3e42c9 | ||
|
02937081bb | ||
|
fba6cf7a1c | ||
|
8d019bd24d | ||
|
30619b393e | ||
|
2a9eefcfd4 | ||
|
e72d296518 | ||
|
d0dbfdd72d | ||
|
8054d87f12 | ||
|
dea850e34d | ||
|
53c61ac0f4 | ||
|
12f4ee4642 | ||
|
f311d9f56a | ||
|
6824e7463a | ||
|
0d741a0d79 | ||
|
917d349d81 | ||
|
614faf75a8 | ||
|
f0209dc096 | ||
|
f98eec69f2 | ||
|
9c7a06bc79 | ||
|
4961fefdde | ||
|
2ddff2692b | ||
|
2f9f27ce89 | ||
|
82cbd80342 | ||
|
1785859faf | ||
|
2175a040fd | ||
|
2fca059502 | ||
|
5cf320723d | ||
|
7876232265 | ||
|
d4117504eb | ||
|
ecf84ee07e | ||
|
2385959936 | ||
|
6383409644 | ||
|
0d93790ec4 | ||
|
452402039e | ||
|
a94009ed4b | ||
|
97ef033b65 | ||
|
6b880ba22d | ||
|
836b9b6a54 | ||
|
c0f53ea712 | ||
|
a5e27385c8 | ||
|
af530f8943 | ||
|
1789db9ade | ||
|
6efa976a56 | ||
|
52bcc1d57b | ||
|
cea6052309 | ||
|
0134b05bb0 | ||
|
28d843d52a | ||
|
be45ab1344 | ||
|
7466b69cce | ||
|
2ed60ba402 | ||
|
d92ac5df6e | ||
|
d6b549cea4 | ||
|
6c8c21ce62 | ||
|
6f97138056 | ||
|
41bb9d817e | ||
|
cb994c47a9 | ||
|
870e13ed4c | ||
|
e1c36405c2 | ||
|
187cc2a97d | ||
|
00ea3c9df3 | ||
|
f6de3a8fdf | ||
|
1ac9598d1b | ||
|
7c79a12942 | ||
|
d783ded672 | ||
|
01dc2beb71 | ||
|
8e6a9be5f7 | ||
|
fd2e0100c6 | ||
|
d3ab4b10e9 | ||
|
e69b9b1faa | ||
|
907959b92e | ||
|
c1b380fc56 | ||
|
03a1bf1dd8 | ||
|
9b78d1cad7 | ||
|
0bb74fcd8d | ||
|
349ac5e41e | ||
|
a4a4130ba1 | ||
|
b593df44b7 | ||
|
28c202431c | ||
|
0c0433a2e0 | ||
|
d35dd2e194 | ||
|
ad0f61391b | ||
|
e7caf3d645 | ||
|
a76eb04d41 | ||
|
31445adc2f | ||
|
75d87fc362 | ||
|
e91682fd16 | ||
|
1430c81f90 | ||
|
c28cbcfcaa | ||
|
48358a7642 | ||
|
d56f24c557 | ||
|
858d36cf30 | ||
|
841810d6ca | ||
|
013e44a9c5 | ||
|
80b1ce33a2 | ||
|
d5424cf8a0 | ||
|
2c84a157e1 | ||
|
dce4ff22e6 | ||
|
4462e39c98 | ||
|
21a2c9c895 | ||
|
0790bfd770 | ||
|
0aa41950ba | ||
|
99a4ac3060 | ||
|
a0511648ab | ||
|
bd67d017c0 | ||
|
1e6328936c | ||
|
fb5b21a880 | ||
|
c13edfb56f | ||
|
89452dd9c5 | ||
|
c3b76a5b44 | ||
|
291ff7b8cf | ||
|
482d64fa04 | ||
|
242d35b142 | ||
|
25989110b1 | ||
|
b4a4d9e78f | ||
|
33901413bf | ||
|
233c434411 | ||
|
dbff7645ed | ||
|
0fb0b4e6ec | ||
|
44adf006cc | ||
|
069bcde2ac | ||
|
55d8ccf61e | ||
|
23ed99ad05 | ||
|
219f6380fb | ||
|
8e1f43093a | ||
|
99ace70150 | ||
|
1c06b66cdc | ||
|
e32191a967 | ||
|
4cb4ca5e96 | ||
|
1484c3585b | ||
|
9bb3070978 | ||
|
8af6ceec9e |
322 changed files with 65062 additions and 14399 deletions
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
custom: [ 'https://v2.hysteria.network/docs/Donation/' ]
|
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report anything you think is a bug and needs to be fixed.
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Logs**
|
||||
Attach logs from the client/server when the error occurs.
|
||||
|
||||
**Device and Operating System**
|
||||
What are you using it on.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
26
.github/ISSUE_TEMPLATE/bug_report.zh.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/bug_report.zh.md
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: Bug 反馈
|
||||
about: 反馈任何你认为是 bug 需要修复的问题。
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述问题**
|
||||
请尽量清晰精准地描述你遇到的问题。
|
||||
|
||||
**如何复现**
|
||||
复现问题的步骤。
|
||||
|
||||
**预期行为**
|
||||
你认为修复后的行为应该是怎样的。
|
||||
|
||||
**日志**
|
||||
附上客户端/服务器端在错误发生前后的日志。
|
||||
|
||||
**设备和操作系统**
|
||||
你在用什么设备和操作系统。
|
||||
|
||||
**额外信息**
|
||||
其他你认为有助于解决问题的信息。
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project.
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
20
.github/ISSUE_TEMPLATE/feature_request.zh.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.zh.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: 功能请求
|
||||
about: 为这个项目提出改进意见。
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**你的功能请求是否与某个问题有关?**
|
||||
请尽量清晰精准地描述你遇到的问题。例如:我家运营商限制 UDP 协议速度,导致 Hysteria 很慢,希望增加 FakeTCP 支持。
|
||||
|
||||
**描述你希望的解决方案**
|
||||
请尽量清晰精准地描述你希望的解决方案。
|
||||
|
||||
**有没有其他替代方案**
|
||||
请尽量清晰精准地描述你认为可能的替代方案。
|
||||
|
||||
**额外信息**
|
||||
其他你认为有助于开发者了解你需求的信息。
|
104
.github/workflows/autotag.yaml
vendored
Normal file
104
.github/workflows/autotag.yaml
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
name: "Create release tags for nested modules"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- app/v*.*.*
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
name: "Create tags"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Extract tagbase"
|
||||
id: extract_tagbase
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const ref = context.ref;
|
||||
core.info(`context.ref: ${ref}`);
|
||||
const refPrefix = 'refs/tags/app/';
|
||||
if (!ref.startsWith(refPrefix)) {
|
||||
core.setFailed(`context.ref does not start with ${refPrefix}: ${ref}`);
|
||||
return;
|
||||
}
|
||||
const tagbase = ref.slice(refPrefix.length);
|
||||
core.info(`tagbase: ${tagbase}`);
|
||||
core.setOutput('tagbase', tagbase);
|
||||
|
||||
- name: "Tagging core/*"
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
INPUT_TAGPREFIX: "core/"
|
||||
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
|
||||
with:
|
||||
script: |
|
||||
const tagbase = core.getInput('tagbase', { required: true });
|
||||
const tagprefix = core.getInput('tagprefix', { required: true });
|
||||
const refname = `tags/${tagprefix}${tagbase}`;
|
||||
core.info(`creating ref ${refname}`);
|
||||
try {
|
||||
await github.rest.git.createRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `refs/${refname}`,
|
||||
sha: context.sha
|
||||
});
|
||||
core.info(`created ref ${refname}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
core.info(`failed to create ref ${refname}: ${error}`);
|
||||
}
|
||||
core.info(`updating ref ${refname}`)
|
||||
try {
|
||||
await github.rest.git.updateRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: refname,
|
||||
sha: context.sha
|
||||
});
|
||||
core.info(`updated ref ${refname}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
core.setFailed(`failed to update ref ${refname}: ${error}`);
|
||||
}
|
||||
|
||||
- name: "Tagging extras/*"
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
INPUT_TAGPREFIX: "extras/"
|
||||
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
|
||||
with:
|
||||
script: |
|
||||
const tagbase = core.getInput('tagbase', { required: true });
|
||||
const tagprefix = core.getInput('tagprefix', { required: true });
|
||||
const refname = `tags/${tagprefix}${tagbase}`;
|
||||
core.info(`creating ref ${refname}`);
|
||||
try {
|
||||
await github.rest.git.createRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `refs/${refname}`,
|
||||
sha: context.sha
|
||||
});
|
||||
core.info(`created ref ${refname}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
core.info(`failed to create ref ${refname}: ${error}`);
|
||||
}
|
||||
core.info(`updating ref ${refname}`)
|
||||
try {
|
||||
await github.rest.git.updateRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: refname,
|
||||
sha: context.sha
|
||||
});
|
||||
core.info(`updated ref ${refname}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
core.setFailed(`failed to update ref ${refname}: ${error}`);
|
||||
}
|
44
.github/workflows/build-master.yml
vendored
44
.github/workflows/build-master.yml
vendored
|
@ -1,44 +0,0 @@
|
|||
name: Build master
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags-ignore:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2.4.0
|
||||
|
||||
- name: Get time
|
||||
uses: gerred/actions/current-time@master
|
||||
id: current-time
|
||||
|
||||
- name: Build
|
||||
uses: crazy-max/ghaction-xgo@v1.6.1
|
||||
env:
|
||||
TIME: "${{ steps.current-time.outputs.time }}"
|
||||
with:
|
||||
xgo_version: latest
|
||||
go_version: latest
|
||||
dest: dist
|
||||
prefix: hysteria
|
||||
targets: linux/amd64,linux/386,linux/arm-5,linux/arm-7,linux/arm64,linux/mipsle,darwin-10.12/amd64,darwin-10.12/arm64,windows-6.0/amd64,windows-6.0/386
|
||||
ldflags: -w -s -X main.appCommit=${{ github.sha }} -X main.appDate=${{ env.TIME }}
|
||||
pkg: cmd
|
||||
|
||||
- name: Archive
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
|
@ -1,71 +0,0 @@
|
|||
# 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"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '17 14 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
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@v2.4.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
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@v1
|
||||
|
||||
# ℹ️ 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@v1
|
|
@ -1,9 +1,9 @@
|
|||
name: Build Docker Image
|
||||
name: "Build Docker Image"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- app/v*.*.*
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
|
@ -13,32 +13,32 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get tag
|
||||
uses: olegtarasov/get-tag@v2
|
||||
id: tagName
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2.7.0
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:${{ env.GIT_TAG_NAME }}
|
||||
tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:v2,tobyxdd/hysteria:${{ steps.get_version.outputs.version }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
52
.github/workflows/master.yml
vendored
Normal file
52
.github/workflows/master.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
name: "Build master branch"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Setup Python # This is for the build script
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r26b
|
||||
add-to-path: false
|
||||
|
||||
- name: Run build script
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",")
|
||||
python hyperbole.py build -r
|
||||
|
||||
- name: Generate hashes
|
||||
run: |
|
||||
for file in build/*; do
|
||||
sha256sum $file >> build/hashes.txt
|
||||
done
|
||||
|
||||
- name: Archive
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: hysteria-master-${{ github.sha }}
|
||||
path: build
|
56
.github/workflows/release-tun.yml
vendored
56
.github/workflows/release-tun.yml
vendored
|
@ -1,56 +0,0 @@
|
|||
name: Build and release (tun)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build and release (tun)
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2.4.0
|
||||
|
||||
- name: Get tag
|
||||
uses: olegtarasov/get-tag@v2
|
||||
id: tagName
|
||||
|
||||
- name: Get time
|
||||
uses: gerred/actions/current-time@master
|
||||
id: current-time
|
||||
|
||||
- name: Build
|
||||
uses: crazy-max/ghaction-xgo@v1.6.1
|
||||
env:
|
||||
TIME: "${{ steps.current-time.outputs.time }}"
|
||||
with:
|
||||
xgo_version: latest
|
||||
go_version: latest
|
||||
dest: dist
|
||||
prefix: hysteria
|
||||
targets: linux/amd64,linux/386,linux/arm-5,linux/arm-7,linux/arm64,linux/mipsle,darwin-10.12/amd64,darwin-10.12/arm64,windows-6.0/amd64,windows-6.0/386
|
||||
ldflags: -w -s -X main.appVersion=${{ env.GIT_TAG_NAME }} -X main.appCommit=${{ github.sha }} -X main.appDate=${{ env.TIME }}
|
||||
pkg: cmd
|
||||
|
||||
- name: Upload
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
./dist/hysteria-linux-amd64
|
||||
./dist/hysteria-linux-386
|
||||
./dist/hysteria-linux-arm-5
|
||||
./dist/hysteria-linux-arm-7
|
||||
./dist/hysteria-linux-arm64
|
||||
./dist/hysteria-linux-mipsle
|
||||
./dist/hysteria-darwin-10.12-amd64
|
||||
./dist/hysteria-darwin-10.12-arm64
|
||||
./dist/hysteria-windows-6.0-amd64.exe
|
||||
./dist/hysteria-windows-6.0-386.exe
|
84
.github/workflows/release.yml
vendored
84
.github/workflows/release.yml
vendored
|
@ -1,51 +1,71 @@
|
|||
name: Build and release
|
||||
name: "Build release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- app/v*.*.*
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build and release
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get tag
|
||||
uses: olegtarasov/get-tag@v2
|
||||
id: tagName
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get time
|
||||
uses: gerred/actions/current-time@master
|
||||
id: current-time
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Build
|
||||
uses: tobyxdd/go-cross-build@25e1ba1da2fb42ed9787b615f0e89235569c58fa
|
||||
- name: Setup Python # This is for the build script
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r26b
|
||||
add-to-path: false
|
||||
|
||||
- name: Run build script
|
||||
env:
|
||||
TIME: "${{ steps.current-time.outputs.time }}"
|
||||
CGO_ENABLED: "0"
|
||||
with:
|
||||
name: hysteria-notun
|
||||
dest: ./dist/
|
||||
ldflags: -w -s -X main.appVersion=${{ env.GIT_TAG_NAME }} -X main.appCommit=${{ github.sha }} -X main.appDate=${{ env.TIME }}
|
||||
platforms: 'linux/amd64, linux/386, linux/arm, linux/arm64, linux/mipsle, darwin/amd64, darwin/arm64, windows/amd64, windows/386'
|
||||
package: ./cmd
|
||||
compress: false
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",")
|
||||
python hyperbole.py build -r
|
||||
|
||||
- name: Upload
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
- name: Generate hashes
|
||||
run: |
|
||||
for file in build/*; do
|
||||
sha256sum $file >> build/hashes.txt
|
||||
done
|
||||
|
||||
- name: Upload GitHub
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
./dist/hysteria-notun-linux-amd64
|
||||
./dist/hysteria-notun-linux-386
|
||||
./dist/hysteria-notun-linux-arm
|
||||
./dist/hysteria-notun-linux-arm64
|
||||
./dist/hysteria-notun-linux-mipsle
|
||||
files: build/*
|
||||
|
||||
- name: Upload CF bucket
|
||||
uses: shallwefootball/upload-s3-action@v1.3.3
|
||||
with:
|
||||
aws_key_id: ${{ secrets.CF_KEY_ID }}
|
||||
aws_secret_access_key: ${{ secrets.CF_KEY }}
|
||||
aws_bucket: "hydownload"
|
||||
endpoint: "https://bea223c61d5a41250d127bd67f51dfec.r2.cloudflarestorage.com/"
|
||||
source_dir: "build"
|
||||
destination_dir: "app/${{ steps.get_version.outputs.version }}"
|
||||
|
||||
- name: Publish to API
|
||||
run: |
|
||||
export HY_API_POST_KEY=${{ secrets.HY2_API_POST_KEY }}
|
||||
pip install requests
|
||||
python hyperbole.py publish
|
||||
|
|
29
.github/workflows/scripts.yml
vendored
Normal file
29
.github/workflows/scripts.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
name: "Publish scripts"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- scripts/**
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
name: Publish scripts to Cloudflare Pages
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: hy2scripts
|
||||
directory: scripts
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: main
|
334
.gitignore
vendored
334
.gitignore
vendored
|
@ -1,7 +1,10 @@
|
|||
# Created by https://www.gitignore.io/api/go,linux,macos,windows,intellij+all
|
||||
# Edit at https://www.gitignore.io/?templates=go,linux,macos,windows,intellij+all
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
|
@ -18,12 +21,11 @@
|
|||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
### GoLand+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
|
@ -33,6 +35,9 @@
|
|||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
|
@ -53,6 +58,9 @@
|
|||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
|
@ -80,6 +88,9 @@ atlassian-ide-plugin.xml
|
|||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
|
@ -92,21 +103,69 @@ fabric.properties
|
|||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### GoLand+all Patch ###
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
.idea/*
|
||||
|
||||
!.idea/codeStyles
|
||||
!.idea/runConfigurations
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
|
||||
# AWS User-specific
|
||||
|
||||
# Generated files
|
||||
|
||||
# Sensitive or high-churn files
|
||||
|
||||
# Gradle
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
|
||||
# Mongo Explorer plugin
|
||||
|
||||
# File-based project format
|
||||
|
||||
# IntelliJ
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
|
||||
# JIRA plugin
|
||||
|
||||
# Cursive Clojure plugin
|
||||
|
||||
# SonarLint plugin
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
|
||||
# Editor-based Rest Client
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
|
||||
### Intellij+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
@ -132,6 +191,7 @@ modules.xml
|
|||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
|
@ -151,6 +211,236 @@ Network Trash Folder
|
|||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### PyCharm+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
|
||||
# AWS User-specific
|
||||
|
||||
# Generated files
|
||||
|
||||
# Sensitive or high-churn files
|
||||
|
||||
# Gradle
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
|
||||
# Mongo Explorer plugin
|
||||
|
||||
# File-based project format
|
||||
|
||||
# IntelliJ
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
|
||||
# JIRA plugin
|
||||
|
||||
# Cursive Clojure plugin
|
||||
|
||||
# SonarLint plugin
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
|
||||
# Editor-based Rest Client
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
|
||||
### PyCharm+all Patch ###
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
|
@ -177,8 +467,4 @@ $RECYCLE.BIN/
|
|||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.gitignore.io/api/go,linux,macos,windows,intellij+all
|
||||
|
||||
cmd/relay/*.json
|
||||
hy_linux
|
||||
.vscode
|
||||
# End of https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all
|
52
ACL.md
52
ACL.md
|
@ -1,52 +0,0 @@
|
|||
# ACL File Format
|
||||
|
||||
ACL files describe how to process incoming requests. Both the server and the client support ACL and follow the identical
|
||||
syntax.
|
||||
|
||||
```
|
||||
action condition_type condition argument
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
direct domain evil.corp
|
||||
proxy domain-suffix google.com
|
||||
block ip 1.2.3.4
|
||||
hijack cidr 192.168.1.1/24 127.0.0.1
|
||||
|
||||
direct all
|
||||
```
|
||||
|
||||
A real-life ACL example of directly connecting to all China IPs (and its generator Python
|
||||
script) [can be found here](docs/acl).
|
||||
|
||||
Hysteria acts according to the first matching rule in the file for each request. When there is no match, the default
|
||||
behavior is to proxy all connections. You can override this by adding a rule at the end of the file with the condition
|
||||
`all`.
|
||||
|
||||
4 actions:
|
||||
|
||||
`direct` - connect directly to the target server without going through the proxy
|
||||
|
||||
`proxy` - connect to the target server through the proxy (only available on the client)
|
||||
|
||||
`block` - block the connection from establishing
|
||||
|
||||
`hijack` - hijack the connection to another target address (must be specified in the argument)
|
||||
|
||||
5 condition types:
|
||||
|
||||
`domain` - match a specific domain (does NOT match subdomains! e.g. `apple.com` will not match `cdn.apple.com`)
|
||||
|
||||
`domain-suffix` - match a domain suffix (match subdomains, but `apple.com` will still not match `fakeapple.com`)
|
||||
|
||||
`cidr` - IPv4 or IPv6 CIDR
|
||||
|
||||
`ip` - IPv4 or IPv6 address
|
||||
|
||||
`all` - match anything (usually placed at the end of the file as a default rule)
|
||||
|
||||
For domain requests, Hysteria will try to resolve the domains and match both domain & IP rules. In other words, an IP
|
||||
rule covers all connections that would end up connecting to this IP, regardless of whether the client requests with IP
|
||||
or domain.
|
46
ACL.zh.md
46
ACL.zh.md
|
@ -1,46 +0,0 @@
|
|||
# ACL 文件格式
|
||||
|
||||
ACL 文件描述如何处理传入请求。服务器和客户端都支持 ACL,并且遵循相同的语法。
|
||||
|
||||
```
|
||||
处理方式 条件类型 条件 参数
|
||||
```
|
||||
|
||||
例子:
|
||||
|
||||
```
|
||||
direct domain evil.corp
|
||||
proxy domain-suffix google.com
|
||||
block ip 1.2.3.4
|
||||
hijack cidr 192.168.1.1/24 127.0.0.1
|
||||
|
||||
direct all
|
||||
```
|
||||
|
||||
一个直连所有中国 IP 的规则和 Python 生成脚本 [在这里](docs/acl)。
|
||||
|
||||
Hysteria 根据文件中第一个匹配到规则对每个请求进行操作。当没有匹配时默认的行为是代理连接。可以通过在文件的末尾添加一个规则加上条件 "all" 来设置默认行为。
|
||||
|
||||
4 种处理方式:
|
||||
|
||||
`direct` - 直接连接到目标服务器,不经过代理
|
||||
|
||||
`proxy` - 通过代理连接到目标服务器(仅在客户端上可用)
|
||||
|
||||
`block` - 拒绝连接建立
|
||||
|
||||
`hijack` - 把连接劫持到另一个目的地 (必须在参数中指定)
|
||||
|
||||
5 种条件类型:
|
||||
|
||||
`domain` - 匹配特定的域名(不匹配子域名!例如:`apple.com` 不匹配 `cdn.apple.com`)
|
||||
|
||||
`domain-suffix` - 匹配域名后缀(包含子域名,但 `apple.com` 仍不会匹配 `fakeapple.com`)
|
||||
|
||||
`cidr` - IPv4 / IPv6 CIDR
|
||||
|
||||
`ip` - IPv4 / IPv6 地址
|
||||
|
||||
`all` - 匹配所有地址 (通常放在文件尾作为默认规则)
|
||||
|
||||
对于域名请求,Hysteria 将尝试解析域名并同时匹配域名规则和 IP 规则。换句话说,IP 规则能覆盖到所有连接,无论客户端是用 IP 还是域名请求。
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -1,27 +1,3 @@
|
|||
# Changelog
|
||||
|
||||
## 0.8.5
|
||||
|
||||
- Added an option to disable MTU discovery `disable_mtu_discovery`
|
||||
|
||||
## 0.8.6
|
||||
|
||||
- Added an option for customizing ALPN `alpn`
|
||||
- Removed ACL support from TPROXY & TUN modes
|
||||
|
||||
## 0.9.0
|
||||
|
||||
- Auto keypair reloading
|
||||
- SOCKS5 listen address no longer needs a specific IP
|
||||
- Multi-relay support
|
||||
- IPv6 only mode for server
|
||||
|
||||
## 0.9.1
|
||||
|
||||
- faketcp implementation
|
||||
- DNS `resolver` option in config
|
||||
|
||||
## 0.9.2
|
||||
|
||||
- Updated quic-go to v0.24.0
|
||||
- Reduced obfs overhead by reusing buffers
|
||||
https://v2.hysteria.network/docs/Changelog/
|
||||
|
|
67
Docker.md
67
Docker.md
|
@ -1,67 +0,0 @@
|
|||
## About Dockerfile
|
||||
|
||||
The hysteria docker image is based on the **alpine** system. This means that
|
||||
**some glibc calls may not work if you run programs that depend on glibc in a container.**
|
||||
|
||||
By default, **bash** is installed in the docker container for debugging, **tzdata** is used to
|
||||
provide container time zone configuration, and **ca-certificates** is used to ensure the
|
||||
trust of the ssl certificate chain; in addition, the docker container does not contain
|
||||
any tools other than the alpine standard system.
|
||||
|
||||
The hysteria binary is installed in `/usr/local/bin/hysteria`, and the **ENTRYPOINT**
|
||||
of the docker container is set to **execute the `hysteria` command**; this means that
|
||||
the `hysteria` command is always the first command.
|
||||
|
||||
## How to use docker image?
|
||||
|
||||
### For standard docker users
|
||||
|
||||
You can mount the configuration file to any location of the docker container and use it.
|
||||
|
||||
In the following commands, we assume that the **`/root/hysteria.json`** configuration
|
||||
file is mounted to **`/etc/hysteria.json`**:
|
||||
|
||||
⚠️ Note: **If you don't want to use the host network (`--network=host`), please make sure that
|
||||
the hysteria UDP port is correctly mapped (`-p 1234:1234/udp`)**
|
||||
|
||||
```sh
|
||||
# Please replace `/root/hysteria.json` with the actual configuration file location
|
||||
docker run -dt --network=host --name hysteria \
|
||||
-v /root/hysteria.json:/etc/hysteria.json \
|
||||
tobyxdd/hysteria -config /etc/hysteria.json server
|
||||
```
|
||||
|
||||
### For docker-compose users
|
||||
|
||||
First, you need to create a directory with any name, and then copy [docker-compose.yaml](https://raw.githubusercontent.com/HyNetwork/hysteria/master/docker-compose.yaml) to
|
||||
that directory. Finally, create your configuration file and start it.
|
||||
|
||||
```sh
|
||||
# Create dir
|
||||
mkdir hysteria && cd hysteria
|
||||
|
||||
# Download the docker-compose example config
|
||||
wget https://raw.githubusercontent.com/HyNetwork/hysteria/master/docker-compose.yaml
|
||||
|
||||
# Create your config
|
||||
cat <<EOF > hysteria.json
|
||||
{
|
||||
"listen": ":36712",
|
||||
"acme": {
|
||||
"domains": [
|
||||
"your.domain.com"
|
||||
],
|
||||
"email": "hacker@gmail.com"
|
||||
},
|
||||
"obfs": "fuck me till the daylight",
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100
|
||||
}
|
||||
EOF
|
||||
|
||||
# Start container
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
|
||||
|
62
Docker.zh.md
62
Docker.zh.md
|
@ -1,62 +0,0 @@
|
|||
## 关于 Dockerfile
|
||||
|
||||
hysteria 的 docker 镜像基于 **alpine** 系统,这意味着如果您在容器里运行一些依赖于 glibc
|
||||
的自定义程序可能会失败。
|
||||
|
||||
默认情况下容器内安装了 **`bash`** 用于调试目的,安装的 **`tzdata`** 用于提供容器的时区信息;
|
||||
为了保证 ACME 等连其他网站时 SSL 证书信任还安装了 **`ca-certificates`**;除此之外容器内不包含
|
||||
任何非标准 alpine 系统的其他工具。
|
||||
|
||||
hysteria 二进制可执行文件默认被安装到 `/usr/local/bin/hysteria`,同时容器的 **ENTRYPOINT**
|
||||
被设置为**执行 `hysteria` 命令**;这意味着在不进行覆盖的情况下容器启动后首先将执行 `hysteria`
|
||||
命令。
|
||||
|
||||
## 如何使用本镜像?
|
||||
|
||||
### 标准 Docker 用户
|
||||
|
||||
您可以将配置文件挂载到容器内的任何位置然后使用它。
|
||||
|
||||
在下面的命令中我们假设将 **`/root/hysteria.json`** 配置文件挂载为容器内的 **`/etc/hysteria.json`** 文件。
|
||||
|
||||
⚠️ 注意: 如果您不想使用宿主机网络(`--network=host`),请确保正确的映射了 hysteria 的 UDP 端口(`-p 1234:1234/udp`)。
|
||||
|
||||
```sh
|
||||
# Please replace `/root/hysteria.json` with the actual configuration file location
|
||||
docker run -dt --network=host --name hysteria \
|
||||
-v /root/hysteria.json:/etc/hysteria.json \
|
||||
tobyxdd/hysteria -config /etc/hysteria.json server
|
||||
```
|
||||
|
||||
### Docker Compose 用户
|
||||
|
||||
首先您需要创建一个任意名称的目录,然后将项目内的 [docker-compose.yaml](https://raw.githubusercontent.com/HyNetwork/hysteria/master/docker-compose.yaml) 文件复制到该目录;
|
||||
最后创建自己的配置文件并启动即可。
|
||||
|
||||
```sh
|
||||
# Create dir
|
||||
mkdir hysteria && cd hysteria
|
||||
|
||||
# Download the docker-compose example config
|
||||
wget https://raw.githubusercontent.com/HyNetwork/hysteria/master/docker-compose.yaml
|
||||
|
||||
# Create your config
|
||||
cat <<EOF > hysteria.json
|
||||
{
|
||||
"listen": ":36712",
|
||||
"acme": {
|
||||
"domains": [
|
||||
"your.domain.com"
|
||||
],
|
||||
"email": "hacker@gmail.com"
|
||||
},
|
||||
"obfs": "fuck me till the daylight",
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100
|
||||
}
|
||||
EOF
|
||||
|
||||
# Start container
|
||||
docker-compose up -d
|
||||
```
|
||||
|
23
Dockerfile
23
Dockerfile
|
@ -1,6 +1,4 @@
|
|||
FROM golang:alpine AS builder
|
||||
|
||||
LABEL maintainer="mritd <mritd@linux.com>"
|
||||
FROM golang:1-alpine AS builder
|
||||
|
||||
# GOPROXY is disabled by default, use:
|
||||
# docker build --build-arg GOPROXY="https://goproxy.io" ...
|
||||
|
@ -9,29 +7,22 @@ ARG GOPROXY=""
|
|||
|
||||
ENV GOPROXY ${GOPROXY}
|
||||
|
||||
COPY . /go/src/github.com/hynetwork/hysteria
|
||||
COPY . /go/src/github.com/apernet/hysteria
|
||||
|
||||
WORKDIR /go/src/github.com/hynetwork/hysteria/cmd
|
||||
WORKDIR /go/src/github.com/apernet/hysteria
|
||||
|
||||
RUN set -ex \
|
||||
&& apk add git build-base \
|
||||
&& export VERSION=$(git describe --tags) \
|
||||
&& export COMMIT=$(git rev-parse HEAD) \
|
||||
&& export TIMESTAMP=$(date "+%F %T") \
|
||||
&& go build -trimpath -o /go/bin/hysteria -ldflags \
|
||||
"-w -s -X 'main.appVersion=${VERSION}' \
|
||||
-X 'main.appCommit=${COMMIT}' \
|
||||
-X 'main.appDate=${TIMESTAMP}'"
|
||||
&& apk add git build-base bash python3 \
|
||||
&& python hyperbole.py build -r \
|
||||
&& mv ./build/hysteria-* /go/bin/hysteria
|
||||
|
||||
# multi-stage builds to create the final image
|
||||
FROM alpine AS dist
|
||||
|
||||
LABEL maintainer="mritd <mritd@linux.com>"
|
||||
|
||||
# set up nsswitch.conf for Go's "netgo" implementation
|
||||
# - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275
|
||||
# - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf
|
||||
RUN [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf
|
||||
RUN if [ ! -e /etc/nsswitch.conf ]; then echo 'hosts: files dns' > /etc/nsswitch.conf; fi
|
||||
|
||||
# bash is used for debugging, tzdata is used to add timezone information.
|
||||
# Install ca-certificates to ensure no CA certificate errors.
|
||||
|
|
17
LICENSE.md
17
LICENSE.md
|
@ -1,16 +1,7 @@
|
|||
The MIT License (MIT)
|
||||
Copyright 2023 Toby
|
||||
|
||||
Copyright (c) 2021 Toby
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
153
PROTOCOL.md
Normal file
153
PROTOCOL.md
Normal file
|
@ -0,0 +1,153 @@
|
|||
# Hysteria 2 Protocol Specification
|
||||
|
||||
Hysteria is a TCP & UDP proxy based on QUIC, designed for speed, security and censorship resistance. This document describes the protocol used by Hysteria starting with version 2.0.0, sometimes internally referred to as the "v4" protocol. From here on, we will call it "the protocol" or "the Hysteria protocol".
|
||||
|
||||
## Requirements Language
|
||||
|
||||
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119).
|
||||
|
||||
## Underlying Protocol & Wire Format
|
||||
|
||||
The Hysteria protocol MUST be implemented on top of the standard QUIC transport protocol [RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000) with [Unreliable Datagram Extension](https://datatracker.ietf.org/doc/rfc9221/).
|
||||
|
||||
All multibyte numbers use Big Endian format.
|
||||
|
||||
All variable-length integers ("varints") are encoded/decoded as defined in QUIC (RFC 9000).
|
||||
|
||||
## Authentication & HTTP/3 masquerading
|
||||
|
||||
One of the key features of the Hysteria protocol is that to a third party without proper authentication credentials (whether it's a middleman or an active prober), a Hysteria proxy server behaves just like a standard HTTP/3 web server. Additionally, the encrypted traffic between the client and the server appears indistinguishable from normal HTTP/3 traffic.
|
||||
|
||||
Therefore, a Hysteria server MUST implement an HTTP/3 server (as defined by [RFC 9114](https://datatracker.ietf.org/doc/rfc9114/)) and handle HTTP requests as any standard web server would. To prevent active probers from detecting common response patterns in Hysteria servers, implementations SHOULD advise users to either host actual content or set it up as a reverse proxy for other sites.
|
||||
|
||||
An actual Hysteria client, upon connection, MUST send the following HTTP/3 request to the server:
|
||||
|
||||
```
|
||||
:method: POST
|
||||
:path: /auth
|
||||
:host: hysteria
|
||||
Hysteria-Auth: [string]
|
||||
Hysteria-CC-RX: [uint]
|
||||
Hysteria-Padding: [string]
|
||||
```
|
||||
|
||||
`Hysteria-Auth`: Authentication credentials.
|
||||
|
||||
`Hysteria-CC-RX`: Client's maximum receive rate in bytes per second. A value of 0 indicates unknown.
|
||||
|
||||
`Hysteria-Padding`: A random padding string of variable length.
|
||||
|
||||
The Hysteria server MUST identify this special request, and, instead of attempting to serve content or forwarding it to an upstream site, it MUST authenticate the client using the provided information. If authentication is successful, the server MUST send the following response (HTTP status code 233):
|
||||
|
||||
```
|
||||
:status: 233 HyOK
|
||||
Hysteria-UDP: [true/false]
|
||||
Hysteria-CC-RX: [uint/"auto"]
|
||||
Hysteria-Padding: [string]
|
||||
```
|
||||
|
||||
`Hysteria-UDP`: Whether the server supports UDP relay.
|
||||
|
||||
`Hysteria-CC-RX`: Server's maximum receive rate in bytes per second. A value of 0 indicates unlimited; "auto" indicates the server refuses to provide a value and ask the client to use congestion control to determine the rate on its own.
|
||||
|
||||
`Hysteria-Padding`: A random padding string of variable length.
|
||||
|
||||
See the Congestion Control section for more information on how to use the `Hysteria-CC-RX` values.
|
||||
|
||||
`Hysteria-Padding` is optional and is only intended to obfuscate the request/response pattern. It SHOULD be ignored by both sides.
|
||||
|
||||
If authentication fails, the server MUST either act like a standard web server that does not understand the request, or in the case of being a reverse proxy, forward the request to the upstream site and return the response to the client.
|
||||
|
||||
The client MUST check the status code to determine if the authentication was successful. If the status code is anything other than 233, the client MUST consider authentication to have failed and disconnect from the server.
|
||||
|
||||
After (and only after) a client passes authentication, the server MUST consider this QUIC connection to be a Hysteria proxy connection. It MUST then start processing proxy requests from the client as described in the next section.
|
||||
|
||||
## Proxy Requests
|
||||
|
||||
### TCP
|
||||
|
||||
For each TCP connection, the client MUST create a new QUIC bidirectional stream and send the following TCPRequest message:
|
||||
|
||||
```
|
||||
[varint] 0x401 (TCPRequest ID)
|
||||
[varint] Address length
|
||||
[bytes] Address string (host:port)
|
||||
[varint] Padding length
|
||||
[bytes] Random padding
|
||||
```
|
||||
|
||||
The server MUST respond with a TCPResponse message:
|
||||
|
||||
```
|
||||
[uint8] Status (0x00 = OK, 0x01 = Error)
|
||||
[varint] Message length
|
||||
[bytes] Message string
|
||||
[varint] Padding length
|
||||
[bytes] Random padding
|
||||
```
|
||||
|
||||
If the status is OK, the server MUST then begin forwarding data between the client and the specified TCP address until either side closes the connection. If the status is Error, the server MUST close the QUIC stream.
|
||||
|
||||
### UDP
|
||||
|
||||
UDP packets MUST be encapsulated in the following UDPMessage format and sent over QUIC's unreliable datagram (for both client-to-server and server-to-client):
|
||||
|
||||
```
|
||||
[uint32] Session ID
|
||||
[uint16] Packet ID
|
||||
[uint8] Fragment ID
|
||||
[uint8] Fragment count
|
||||
[varint] Address length
|
||||
[bytes] Address string (host:port)
|
||||
[bytes] Payload
|
||||
```
|
||||
|
||||
The client MUST use a unique Session ID for each UDP session. The server SHOULD assign a unique UDP port to each Session ID, unless it has another mechanism to differentiate packets from different sessions (e.g., symmetric NAT, varying outbound IP addresses, etc.).
|
||||
|
||||
The protocol does not provide an explicit way to close a UDP session. While a client can retain and reuse a Session ID indefinitely, the server SHOULD release and reassign the port associated with the Session ID after a period of inactivity or some other criteria. If the client sends a UDP packet to a Session ID that is no longer recognized by the server, the server MUST treat it as a new session and assign a new port.
|
||||
|
||||
If a server does not support UDP relay, it SHOULD silently discard all UDP messages received from the client.
|
||||
|
||||
#### Fragmentation
|
||||
|
||||
Due to the limit imposed by QUIC's unreliable datagram channel, any UDP packet that exceeds QUIC's maximum datagram size MUST either be fragmented or discarded.
|
||||
|
||||
For fragmented packets, each fragment MUST carry the same unique Packet ID. The Fragment ID, starting from 0, indicates the index out of the total Fragment Count. Both the server and client MUST wait for all fragments of a fragmented packet to arrive before processing them. If one or more fragments of a packet are lost, the entire packet MUST be discarded.
|
||||
|
||||
For packets that are not fragmented, the Fragment Count MUST be set to 1. In this case, the values of Packet ID and Fragment ID are irrelevant.
|
||||
|
||||
## Congestion Control
|
||||
|
||||
A unique feature of Hysteria is the ability to set the tx/rx (upload/download) rate on the client side. During authentication, the client sends its rx rate to the server via the `Hysteria-CC-RX` header. The server can use this to determine its transmission rate to the client, and vice versa by returning its rx rate to the client through the same header.
|
||||
|
||||
Three special cases are:
|
||||
|
||||
- If the client sends 0, it doesn't know its own rx rate. The server MUST use a congestion control algorithm (e.g., BBR, Cubic) to adjust its transmission rate.
|
||||
- If the server responds with 0, it has no bandwidth limit. The client MAY transmit at any rate it wants.
|
||||
- If the server responds with "auto", it chooses not to specify a rate. The client MUST use a congestion control algorithm to adjust its transmission rate.
|
||||
|
||||
## "Salamander" Obfuscation
|
||||
|
||||
The Hysteria protocol supports an optional obfuscation layer codenamed "Salamander".
|
||||
|
||||
"Salamander" encapsulates all QUIC packets in the following format:
|
||||
|
||||
```
|
||||
[8 bytes] Salt
|
||||
[bytes] Payload
|
||||
```
|
||||
|
||||
For each QUIC packet, the obfuscator MUST calculate the BLAKE2b-256 hash of a randomly generated 8-byte salt appended to a user-provided pre-shared key.
|
||||
|
||||
```
|
||||
hash = BLAKE2b-256(key + salt)
|
||||
```
|
||||
|
||||
The hash is then used to obfuscate the payload using the following algorithm:
|
||||
|
||||
```
|
||||
for i in range(0, len(payload)):
|
||||
payload[i] ^= hash[i % 32]
|
||||
```
|
||||
|
||||
The deobfuscator MUST use the same algorithms to calculate the salted hash and deobfuscate the payload. Any invalid packet MUST be discarded.
|
509
README.md
509
README.md
|
@ -1,487 +1,60 @@
|
|||
# 
|
||||
# 
|
||||
|
||||
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6]
|
||||
|
||||
[1]: https://img.shields.io/github/license/tobyxdd/hysteria?style=flat-square
|
||||
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6] [![Discussions][7]][8]
|
||||
|
||||
[1]: https://img.shields.io/badge/license-MIT-blue
|
||||
[2]: LICENSE.md
|
||||
|
||||
[3]: https://img.shields.io/github/v/release/tobyxdd/hysteria?style=flat-square
|
||||
|
||||
[4]: https://github.com/tobyxdd/hysteria/releases
|
||||
|
||||
[3]: https://img.shields.io/github/v/release/apernet/hysteria?style=flat-square
|
||||
[4]: https://github.com/apernet/hysteria/releases
|
||||
[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square
|
||||
|
||||
[6]: https://t.me/hysteria_github
|
||||
[7]: https://img.shields.io/github/discussions/apernet/hysteria?style=flat-square
|
||||
[8]: https://github.com/apernet/hysteria/discussions
|
||||
|
||||
[中文](README.zh.md)
|
||||
<h2 style="text-align: center;">Hysteria is a powerful, lightning fast and censorship resistant proxy.</h2>
|
||||
|
||||
Hysteria is a feature-packed network utility optimized for networks of poor quality (e.g. satellite connections,
|
||||
congested public Wi-Fi, connecting from China to servers abroad) powered by a custom version of QUIC protocol. It
|
||||
currently has the following features: (still growing!)
|
||||
### [Get Started](https://v2.hysteria.network/)
|
||||
|
||||
- SOCKS5 proxy (TCP & UDP)
|
||||
- HTTP/HTTPS proxy
|
||||
- TCP/UDP relay
|
||||
- TCP/UDP TPROXY (Linux only)
|
||||
- TUN (TAP on Windows)
|
||||
### [中文文档](https://v2.hysteria.network/zh/)
|
||||
|
||||
## Installation
|
||||
### [Hysteria 1.x (legacy)](https://v1.hysteria.network/)
|
||||
|
||||
### Windows, Linux, macOS CLI
|
||||
---
|
||||
|
||||
- Download pre-built binaries from https://github.com/tobyxdd/hysteria/releases
|
||||
- Linux builds are available as `hysteria` (with tun support) and `hysteria-notun` (without tun support). Builds
|
||||
without tun support are statically linked and do not depend on glibc. **If you use a non-standard distribution that
|
||||
can't run `hysteria` properly, try `hysteria-notun` instead.**
|
||||
- Use Docker or Docker Compose: https://github.com/HyNetwork/hysteria/blob/master/Docker.md
|
||||
- Use our Arch Linux AUR: https://aur.archlinux.org/packages/hysteria/
|
||||
- Build from source with `go build ./cmd`
|
||||
<div class="feature-grid">
|
||||
<div>
|
||||
<h3>🛠️ Jack of all trades</h3>
|
||||
<p>Wide range of modes including SOCKS5, HTTP Proxy, TCP/UDP Forwarding, Linux TProxy, TUN - with more features being added constantly.</p>
|
||||
</div>
|
||||
|
||||
### OpenWrt LuCI app
|
||||
<div>
|
||||
<h3>⚡ Blazing fast</h3>
|
||||
<p>Powered by a customized QUIC protocol, Hysteria is designed to deliver unparalleled performance over unreliable and lossy networks.</p>
|
||||
</div>
|
||||
|
||||
- [openwrt-passwall](https://github.com/xiaorouji/openwrt-passwall)
|
||||
<div>
|
||||
<h3>✊ Censorship resistant</h3>
|
||||
<p>The protocol masquerades as standard HTTP/3 traffic, making it very difficult for censors to detect and block without widespread collateral damage.</p>
|
||||
</div>
|
||||
|
||||
### Android
|
||||
<div>
|
||||
<h3>💻 Cross-platform</h3>
|
||||
<p>We have builds for every major platform and architecture. Deploy anywhere & use everywhere. Not to mention the long list of 3rd party apps.</p>
|
||||
</div>
|
||||
|
||||
- [SagerNet](https://github.com/SagerNet/SagerNet) with [hysteria-plugin](https://github.com/SagerNet/SagerNet/releases/tag/hysteria-plugin-0.9.1)
|
||||
<div>
|
||||
<h3>🔗 Easy integration</h3>
|
||||
<p>With built-in support for custom authentication, traffic statistics & access control, Hysteria is easy to integrate into your infrastructure.</p>
|
||||
</div>
|
||||
|
||||
### iOS
|
||||
<div>
|
||||
<h3>🤗 Chill and supportive</h3>
|
||||
<p>We have well-documented specifications and code for developers to contribute and/or build their own apps. And a helpful community, too.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
- [Shadowrocket](https://apps.apple.com/us/app/shadowrocket/id932747118)
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
**If you find Hysteria useful, consider giving it a ⭐️!**
|
||||
|
||||
Note: This is only a bare-bones example to get the server and client running. Go to [Advanced usage](#advanced-usage)
|
||||
for all the available options.
|
||||
|
||||
### Server
|
||||
|
||||
Create a `config.json` under the root directory of the program:
|
||||
|
||||
```json
|
||||
{
|
||||
"listen": ":36712",
|
||||
"acme": {
|
||||
"domains": [
|
||||
"your.domain.com"
|
||||
],
|
||||
"email": "hacker@gmail.com"
|
||||
},
|
||||
"obfs": "fuck me till the daylight",
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100
|
||||
}
|
||||
```
|
||||
|
||||
Hysteria requires a TLS certificate. You can either get a trusted TLS certificate from Let's Encrypt automatically using
|
||||
the built-in ACME integration, or provide it yourself. It does not have to be valid and trusted, but in that case the
|
||||
clients need additional configuration. To use your own existing TLS certificate, refer to this config:
|
||||
|
||||
```json
|
||||
{
|
||||
"listen": ":36712",
|
||||
"cert": "/home/ubuntu/my.crt",
|
||||
"key": "/home/ubuntu/my.key",
|
||||
"obfs": "fuck me till the daylight",
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100
|
||||
}
|
||||
```
|
||||
|
||||
The (optional) `obfs` option obfuscates the protocol using the provided password, so that it is not apparent that this
|
||||
is Hysteria/QUIC, which could be useful for bypassing DPI blocking or QoS. If the passwords of the server and client do
|
||||
not match, no connection can be established. Therefore, this can also serve as a simple password authentication. For
|
||||
more advanced authentication schemes, see `auth` below.
|
||||
|
||||
`up_mbps` and `down_mbps` limit the maximum upload and download speed of the server for each client. Feel free to remove
|
||||
them if you don't need.
|
||||
|
||||
To launch the server, simply run
|
||||
|
||||
```
|
||||
./hysteria-linux-amd64 server
|
||||
```
|
||||
|
||||
If your config file is not named `config.json` or is in a different path, specify it with `-config`:
|
||||
|
||||
```
|
||||
./hysteria-linux-amd64 -config blah.json server
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
Same as the server side, create a `config.json` under the root directory of the program:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": "example.com:36712",
|
||||
"obfs": "fuck me till the daylight",
|
||||
"up_mbps": 10,
|
||||
"down_mbps": 50,
|
||||
"socks5": {
|
||||
"listen": "127.0.0.1:1080"
|
||||
},
|
||||
"http": {
|
||||
"listen": "127.0.0.1:8080"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This config enables a SOCKS5 proxy (with both TCP & UDP support), and an HTTP proxy at the same time. There are many
|
||||
other modes in Hysteria, be sure to check them out in [Advanced usage](#advanced-usage)! To enable or disable a mode,
|
||||
simply add or remove its entry in the config file.
|
||||
|
||||
If your server certificate is not issued by a trusted CA, you need to specify the CA used
|
||||
with `"ca": "/path/to/file.ca"` on the client or use `"insecure": true` to ignore all certificate errors (not
|
||||
recommended).
|
||||
|
||||
`up_mbps` and `down_mbps` are mandatory on the client side. Try to fill in these values as accurately as possible
|
||||
according to your network conditions, as they are crucial for Hysteria to work optimally.
|
||||
|
||||
Some users may attempt to forward other encrypted proxy protocols such as Shadowsocks with relay. While this technically
|
||||
works, it's not optimal from a performance standpoint - Hysteria itself uses TLS, considering that the proxy protocol
|
||||
being forwarded is also encrypted, and the fact that almost all sites are now using HTTPS, it essentially becomes triple
|
||||
encryption. If you need a proxy, just use our proxy modes.
|
||||
|
||||
## Comparison
|
||||
|
||||

|
||||
|
||||
## Advanced usage
|
||||
|
||||
### Server
|
||||
|
||||
```json5
|
||||
{
|
||||
"listen": ":36712", // Listen address
|
||||
"protocol": "faketcp", // Blank or "udp" for UDP mode, "faketcp" for TCP "masquerade", see below for details
|
||||
"acme": {
|
||||
"domains": [
|
||||
"your.domain.com",
|
||||
"another.domain.net"
|
||||
], // Domains for the ACME cert
|
||||
"email": "hacker@gmail.com", // Registration email, optional but recommended
|
||||
"disable_http": false, // Disable HTTP challenges
|
||||
"disable_tlsalpn": false, // Disable TLS-ALPN challenges
|
||||
"alt_http_port": 8080, // Alternate port for HTTP challenges
|
||||
"alt_tlsalpn_port": 4433 // Alternate port for TLS-ALPN challenges
|
||||
},
|
||||
"cert": "/home/ubuntu/my_cert.crt", // Cert file, mutually exclusive with the ACME options above
|
||||
"key": "/home/ubuntu/my_key.crt", // Key file, mutually exclusive with the ACME options above
|
||||
"up_mbps": 100, // Max upload Mbps per client
|
||||
"down_mbps": 100, // Max download Mbps per client
|
||||
"disable_udp": false, // Disable UDP support
|
||||
"acl": "my_list.acl", // See ACL below
|
||||
"obfs": "AMOGUS", // Obfuscation password
|
||||
"auth": { // Authentication
|
||||
"mode": "password", // Mode, supports "password" "none" and "external" for now
|
||||
"config": {
|
||||
"password": "yubiyubi"
|
||||
}
|
||||
},
|
||||
"alpn": "ayaya", // QUIC TLS ALPN
|
||||
"prometheus_listen": ":8080", // Prometheus HTTP metrics server listen address (at /metrics)
|
||||
"recv_window_conn": 15728640, // QUIC stream receive window
|
||||
"recv_window_client": 67108864, // QUIC connection receive window
|
||||
"max_conn_client": 4096, // Max concurrent connections per client
|
||||
"disable_mtu_discovery": false, // Disable Path MTU Discovery (RFC 8899)
|
||||
"ipv6_only": false, // Only resolve domains to IPv6 address
|
||||
"resolver": "1.1.1.1:53" // DNS resolver address
|
||||
}
|
||||
```
|
||||
|
||||
#### ACME
|
||||
|
||||
Only HTTP and TLS-ALPN challenges are currently supported (no DNS challenges). Make sure your TCP ports 80/443 are
|
||||
accessible respectively.
|
||||
|
||||
#### External authentication integration
|
||||
|
||||
If you are a commercial proxy provider, you may want to connect Hysteria to your own authentication backend.
|
||||
|
||||
```json5
|
||||
{
|
||||
// ...
|
||||
"auth": {
|
||||
"mode": "external",
|
||||
"config": {
|
||||
"http": "https://api.example.com/auth" // Both HTTP and HTTPS are supported
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For the above config, Hysteria sends a POST request to `https://api.example.com/auth` upon each client's connection:
|
||||
|
||||
```json5
|
||||
{
|
||||
"addr": "111.222.111.222:52731",
|
||||
"payload": "[BASE64]", // auth or auth_str of the client
|
||||
"send": 12500000, // Negotiated server send speed for this client (Bps)
|
||||
"recv": 12500000 // Negotiated server recv speed for this client (Bps)
|
||||
}
|
||||
```
|
||||
|
||||
The endpoint must return results with HTTP status code 200 (even if the authentication failed):
|
||||
|
||||
```json5
|
||||
{
|
||||
"ok": false,
|
||||
"msg": "No idea who you are"
|
||||
}
|
||||
```
|
||||
|
||||
`ok` indicates whether the authentication passed. `msg` is a success/failure message.
|
||||
|
||||
#### Prometheus Metrics
|
||||
|
||||
You can make Hysteria expose a Prometheus HTTP client endpoint for monitoring traffic usage with `prometheus_listen`.
|
||||
If configured on port 8080, the endpoint would be at `http://example.com:8080/metrics`.
|
||||
|
||||
```text
|
||||
hysteria_active_conn{auth="55m95auW5oCq"} 32
|
||||
hysteria_active_conn{auth="aGFja2VyISE="} 7
|
||||
|
||||
hysteria_traffic_downlink_bytes_total{auth="55m95auW5oCq"} 122639
|
||||
hysteria_traffic_downlink_bytes_total{auth="aGFja2VyISE="} 3.225058e+06
|
||||
|
||||
hysteria_traffic_uplink_bytes_total{auth="55m95auW5oCq"} 40710
|
||||
hysteria_traffic_uplink_bytes_total{auth="aGFja2VyISE="} 37452
|
||||
```
|
||||
|
||||
`auth` is the auth payload sent by the clients, encoded in Base64.
|
||||
|
||||
### Client
|
||||
|
||||
```json5
|
||||
{
|
||||
"server": "example.com:36712", // Server address
|
||||
"protocol": "faketcp", // Blank or "udp" for UDP mode, "faketcp" for TCP "masquerade", see below for details
|
||||
"up_mbps": 10, // Max upload Mbps
|
||||
"down_mbps": 50, // Max download Mbps
|
||||
"socks5": {
|
||||
"listen": "127.0.0.1:1080", // SOCKS5 listen address
|
||||
"timeout": 300, // TCP timeout in seconds
|
||||
"disable_udp": false, // Disable UDP support
|
||||
"user": "me", // SOCKS5 authentication username
|
||||
"password": "lmaolmao" // SOCKS5 authentication password
|
||||
},
|
||||
"http": {
|
||||
"listen": "127.0.0.1:8080", // HTTP listen address
|
||||
"timeout": 300, // TCP timeout in seconds
|
||||
"user": "me", // HTTP authentication username
|
||||
"password": "lmaolmao", // HTTP authentication password
|
||||
"cert": "/home/ubuntu/my_cert.crt", // Cert file (HTTPS proxy)
|
||||
"key": "/home/ubuntu/my_key.crt" // Key file (HTTPS proxy)
|
||||
},
|
||||
"tun": {
|
||||
"name": "tun-hy", // TUN interface name
|
||||
"timeout": 300, // Timeout in seconds
|
||||
"address": "192.0.2.2", // TUN interface address, not applicable for Linux
|
||||
"gateway": "192.0.2.1", // TUN interface gateway, not applicable for Linux
|
||||
"mask": "255.255.255.252", // TUN interface mask, not applicable for Linux
|
||||
"dns": [ "8.8.8.8", "8.8.4.4" ], // TUN interface DNS, only applicable for Windows
|
||||
"persist": false // Persist TUN interface after exit, only applicable for Linux
|
||||
},
|
||||
"relay_tcps": [
|
||||
{
|
||||
"listen": "127.0.0.1:2222", // TCP relay listen address
|
||||
"remote": "123.123.123.123:22", // TCP relay remote address
|
||||
"timeout": 300 // TCP timeout in seconds
|
||||
},
|
||||
{
|
||||
"listen": "127.0.0.1:13389", // TCP relay listen address
|
||||
"remote": "124.124.124.124:3389", // TCP relay remote address
|
||||
"timeout": 300 // TCP timeout in seconds
|
||||
}
|
||||
],
|
||||
"relay_udps": [
|
||||
{
|
||||
"listen": "127.0.0.1:5333", // UDP relay listen address
|
||||
"remote": "8.8.8.8:53", // UDP relay remote address
|
||||
"timeout": 60 // UDP session timeout in seconds
|
||||
},
|
||||
{
|
||||
"listen": "127.0.0.1:11080", // UDP relay listen address
|
||||
"remote": "9.9.9.9.9:1080", // UDP relay remote address
|
||||
"timeout": 60 // UDP session timeout in seconds
|
||||
}
|
||||
],
|
||||
"tproxy_tcp": {
|
||||
"listen": "127.0.0.1:9000", // TCP TProxy listen address
|
||||
"timeout": 300 // TCP timeout in seconds
|
||||
},
|
||||
"tproxy_udp": {
|
||||
"listen": "127.0.0.1:9000", // UDP TProxy listen address
|
||||
"timeout": 60 // UDP session timeout in seconds
|
||||
},
|
||||
"acl": "my_list.acl", // See ACL below
|
||||
"obfs": "AMOGUS", // Obfuscation password
|
||||
"auth": "[BASE64]", // Authentication payload in Base64
|
||||
"auth_str": "yubiyubi", // Authentication payload in string, mutually exclusive with the option above
|
||||
"alpn": "ayaya", // QUIC TLS ALPN
|
||||
"server_name": "real.name.com", // TLS hostname used to verify the server certificate
|
||||
"insecure": false, // Ignore all certificate errors
|
||||
"ca": "my.ca", // Custom CA file
|
||||
"recv_window_conn": 15728640, // QUIC stream receive window
|
||||
"recv_window": 67108864, // QUIC connection receive window
|
||||
"disable_mtu_discovery": false, // Disable Path MTU Discovery (RFC 8899)
|
||||
"resolver": "1.1.1.1:53" // DNS resolver address
|
||||
}
|
||||
```
|
||||
|
||||
#### Fake TCP / TCP masquerade
|
||||
|
||||
Certain networks may impose various restrictions on UDP traffic or block it altogether. Hysteria offers a "faketcp" mode
|
||||
that allows servers and clients to communicate using a protocol that looks like TCP but does not actually go through the
|
||||
system TCP stack. This tricks whatever middleboxes into thinking it's actually TCP traffic, rendering UDP-specific
|
||||
restrictions useless.
|
||||
|
||||
This mode is currently only supported on Linux (both client and server) and requires root privileges.
|
||||
|
||||
If your server is behind a firewall, open the corresponding TCP port instead of UDP.
|
||||
|
||||
#### Transparent proxy
|
||||
|
||||
TPROXY modes (`tproxy_tcp` & `tproxy_udp`) are only available on Linux.
|
||||
|
||||
References:
|
||||
- https://www.kernel.org/doc/Documentation/networking/tproxy.txt
|
||||
- https://powerdns.org/tproxydoc/tproxy.md.html
|
||||
|
||||
## Optimization tips
|
||||
|
||||
### Optimizing for extreme transfer speeds
|
||||
|
||||
If you want to use Hysteria for very high speed transfers (e.g. 10GE, 1G+ over inter-country long fat pipes), consider
|
||||
increasing your system's UDP receive buffer size.
|
||||
|
||||
```shell
|
||||
sysctl -w net.core.rmem_max=4000000
|
||||
```
|
||||
|
||||
This would increase the buffer size to roughly 4 MB on Linux.
|
||||
|
||||
You may also need to increase `recv_window_conn` and `recv_window` (`recv_window_client` on server side) to make sure
|
||||
they are at least no less than the bandwidth-delay product. For example, if you want to achieve a transfer speed of 500
|
||||
MB/s on a line with an RTT of 200 ms, you need a minimum receive window size of 100 MB (500*0.2).
|
||||
|
||||
### Routers and other embedded devices
|
||||
|
||||
For devices with very limited computing power and RAM, turning off obfuscation can bring a slight performance boost.
|
||||
|
||||
The default receive window size for both Hysteria server and client is 64 MB. Consider lowering them if it's too large
|
||||
for your device. Keeping a ratio of one to four between stream receive window and connection receive window is
|
||||
recommended.
|
||||
|
||||
## ACL
|
||||
|
||||
[ACL File Format](ACL.md)
|
||||
|
||||
ACL is available on both client & server. On the server side it can be used to restrict what the clients can access, and
|
||||
is valid for any mode on the client side. On the client side, it's only supported in SOCKS5 & HTTP proxy modes, and has
|
||||
no effect in other modes (all traffic will go through the proxy)
|
||||
|
||||
## URI Scheme
|
||||
|
||||
Third party clients looking to implement a "share by link" feature are advised to follow the following URI scheme
|
||||
(initially introduced by Shadowrocket):
|
||||
|
||||
hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks
|
||||
|
||||
- host: hostname or IP address of the server to connect to (required)
|
||||
- port: port of the server to connect to (required)
|
||||
- protocol: protocol to use ("udp" or "faketcp") (optional, default: "udp")
|
||||
- auth: authentication payload (string) (optional)
|
||||
- peer: SNI for TLS (optional)
|
||||
- insecure: ignore certificate errors (optional)
|
||||
- upmbps: upstream bandwidth in Mbps (required)
|
||||
- downmbps: downstream bandwidth in Mbps (required)
|
||||
- alpn: QUIC ALPN (optional)
|
||||
- obfs: Obfuscation mode (optional, empty or "xplus")
|
||||
- obfsParam: Obfuscation password (optional)
|
||||
- remarks: remarks (optional)
|
||||
|
||||
## Logging
|
||||
|
||||
The program outputs `DEBUG` level, text format logs via stdout by default.
|
||||
|
||||
To change the logging level, use `LOGGING_LEVEL` environment variable. The available levels are `panic`, `fatal`
|
||||
, `error`, `warn`, `info`, ` debug`, `trace`
|
||||
|
||||
To print JSON instead, set `LOGGING_FORMATTER` to `json`
|
||||
|
||||
To change the logging timestamp format, set `LOGGING_TIMESTAMP_FORMAT`
|
||||
|
||||
## Custom CA
|
||||
|
||||
1. Suppose the server address is `123.123.123.123`, UDP port `5678` is not blocked by firewall
|
||||
2. openssl is already installed
|
||||
3. hysteria is already installed in `/root/hysteria/` directory
|
||||
<details>
|
||||
<summary>4. Generate custom CA certificate</summary>
|
||||
|
||||
- Run below shell in `/root/hysteria/` folder
|
||||
|
||||
``` shell
|
||||
#!/usr/bin/env bash
|
||||
|
||||
domain=$(openssl rand -hex 8)
|
||||
password=$(openssl rand -hex 16)
|
||||
obfs=$(openssl rand -hex 6)
|
||||
path="/root/hysteria"
|
||||
|
||||
openssl genrsa -out hysteria.ca.key 2048
|
||||
|
||||
openssl req -new -x509 -days 3650 -key hysteria.ca.key -subj "/C=CN/ST=GD/L=SZ/O=Hysteria, Inc./CN=Hysteria Root CA" -out hysteria.ca.crt
|
||||
|
||||
openssl req -newkey rsa:2048 -nodes -keyout hysteria.server.key -subj "/C=CN/ST=GD/L=SZ/O=Hysteria, Inc./CN=*.${domain}.com" -out hysteria.server.csr
|
||||
|
||||
openssl x509 -req -extfile <(printf "subjectAltName=DNS:${domain}.com,DNS:www.${domain}.com") -days 3650 -in hysteria.server.csr -CA hysteria.ca.crt -CAkey hysteria.ca.key -CAcreateserial -out hysteria.server.crt
|
||||
|
||||
cat > ./client.json <<EOF
|
||||
{
|
||||
"server": "123.123.123.123:5678",
|
||||
"alpn": "h3",
|
||||
"obfs": "${obfs}",
|
||||
"auth_str": "${password}",
|
||||
"up_mbps": 30,
|
||||
"down_mbps": 30,
|
||||
"socks5": {
|
||||
"listen": "0.0.0.0:1080"
|
||||
},
|
||||
"http": {
|
||||
"listen": "0.0.0.0:8080"
|
||||
},
|
||||
"server_name": "www.${domain}.com",
|
||||
"ca": "${path}/hysteria.ca.crt"
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
cat > ./server.json <<EOF
|
||||
{
|
||||
"listen": ":5678",
|
||||
"alpn": "h3",
|
||||
"obfs": "${obfs}",
|
||||
"cert": "${path}/hysteria.server.crt",
|
||||
"key": "${path}/hysteria.server.key" ,
|
||||
"auth": {
|
||||
"mode": "password",
|
||||
"config": {
|
||||
"password": "${password}"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
</details>
|
||||
|
||||
5. Server side: copy `server.json`、 `hysteria.server.crt`、 `hysteria.server.key` to `/root/hysteria/` directory, run `/root/hysteria/hysteria -c /root/hysteria/server.json server` command
|
||||
|
||||
6. Client side: Assuming that the client directory is also`/root/hysteria`, copy `client.json`、`hysteria.ca.crt` to `/root/hysteria/` directory, run `/root/hysteria/hysteria -c /root/hysteria/client.json` cmmand
|
||||
|
||||
7. After generating CA certificate, modify the server address, port and certificate file path according to your own situation, add obfs and alpn to prevent the first time to be walled in some environment, after the first test passed in full parameters, you can remove the unnecessary parameters such as obfs and alpn in your own network environment.
|
||||
|
||||
8. If you are using shadowrocket on IOS, you can airdrop the file `hysteria.ca.crt` to your iPhone and install it, then you can use custom CA certificate.
|
||||
[](https://star-history.com/#apernet/hysteria&Date)
|
||||
|
|
466
README.zh.md
466
README.zh.md
|
@ -1,466 +0,0 @@
|
|||
# 
|
||||
|
||||
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6]
|
||||
|
||||
[1]: https://img.shields.io/github/license/tobyxdd/hysteria?style=flat-square
|
||||
|
||||
[2]: LICENSE.md
|
||||
|
||||
[3]: https://img.shields.io/github/v/release/tobyxdd/hysteria?style=flat-square
|
||||
|
||||
[4]: https://github.com/tobyxdd/hysteria/releases
|
||||
|
||||
[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square
|
||||
|
||||
[6]: https://t.me/hysteria_github
|
||||
|
||||
Hysteria 是一个功能丰富的,专为恶劣网络环境进行优化的网络工具(双边加速),比如卫星网络、拥挤的公共 Wi-Fi、在中国连接国外服务器等。
|
||||
基于修改版的 QUIC 协议。目前有以下模式:(仍在增加中)
|
||||
|
||||
- SOCKS5 代理 (TCP & UDP)
|
||||
- HTTP/HTTPS 代理
|
||||
- TCP/UDP 转发
|
||||
- TCP/UDP TPROXY 透明代理 (Linux)
|
||||
- TUN (Windows 下为 TAP)
|
||||
|
||||
## 下载安装
|
||||
|
||||
### Windows, Linux, macOS CLI
|
||||
|
||||
- 从 https://github.com/tobyxdd/hysteria/releases 下载编译好的版本
|
||||
- Linux 分为 `hysteria` (带有 tun 支持) 和 `hysteria-notun` (无 tun 支持) 两个版本。无 tun 支持的版本是静态链接,不依赖系统
|
||||
glibc 的。**如果你使用了非标准 Linux 发行版,无法正常执行 `hysteria`,可尝试 `hysteria-notun`**
|
||||
- 使用 Docker 或 Docker Compose: https://github.com/HyNetwork/hysteria/blob/master/Docker.zh.md
|
||||
- 使用 Arch Linux AUR: https://aur.archlinux.org/packages/hysteria/
|
||||
- 自己用 `go build ./cmd` 从源码编译
|
||||
|
||||
### OpenWrt LuCI app
|
||||
|
||||
- [openwrt-passwall](https://github.com/xiaorouji/openwrt-passwall)
|
||||
|
||||
### Android
|
||||
|
||||
- [SagerNet](https://github.com/SagerNet/SagerNet) 配合 [hysteria-plugin](https://github.com/SagerNet/SagerNet/releases/tag/hysteria-plugin-0.9.1)
|
||||
|
||||
### iOS
|
||||
|
||||
- [Shadowrocket](https://apps.apple.com/us/app/shadowrocket/id932747118)
|
||||
|
||||
## 快速入门
|
||||
|
||||
注意:本节提供的配置只是为了快速上手,可能无法满足你的需求。请到 [高级用法](#高级用法) 中查看所有可用选项与含义。
|
||||
|
||||
### 服务器
|
||||
|
||||
在目录下建立一个 `config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"listen": ":36712",
|
||||
"acme": {
|
||||
"domains": [
|
||||
"your.domain.com"
|
||||
],
|
||||
"email": "hacker@gmail.com"
|
||||
},
|
||||
"obfs": "fuck me till the daylight",
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100
|
||||
}
|
||||
```
|
||||
|
||||
服务端需要一个 TLS 证书。 你可以让 Hysteria 内置的 ACME 尝试自动从 Let's Encrypt 为你的服务器签发一个证书,也可以自己提供。
|
||||
证书未必一定要是有效、可信的,但在这种情况下客户端需要进行额外的配置。要使用自己的 TLS 证书,参考这个配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"listen": ":36712",
|
||||
"cert": "/home/ubuntu/my.crt",
|
||||
"key": "/home/ubuntu/my.key",
|
||||
"obfs": "fuck me till the daylight",
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100
|
||||
}
|
||||
```
|
||||
|
||||
可选的 `obfs` 选项使用提供的密码对协议进行混淆,这样协议就不容易被检测出是 Hysteria/QUIC,可以用来绕过针对性的 DPI 屏蔽或者 QoS。
|
||||
如果服务端和客户端的密码不匹配就不能建立连接,因此这也可以作为一个简单的密码验证。对于更高级的验证方案请见下文 `auth`。
|
||||
|
||||
`up_mbps` 和 `down_mbps` 限制服务器对每个客户端的最大上传和下载速度。这些也是可选的,如果不需要可以移除。
|
||||
|
||||
要启动服务端,只需运行
|
||||
|
||||
```
|
||||
./hysteria-linux-amd64 server
|
||||
```
|
||||
|
||||
如果你的配置文件没有命名为 `config.json` 或在别的路径,请用 `-config` 指定路径:
|
||||
|
||||
```
|
||||
./hysteria-linux-amd64 -config blah.json server
|
||||
```
|
||||
|
||||
### 客户端
|
||||
|
||||
和服务器端一样,在程序根目录下建立一个`config.json`。
|
||||
|
||||
```json
|
||||
{
|
||||
"server": "example.com:36712",
|
||||
"obfs": "fuck me till the daylight",
|
||||
"up_mbps": 10,
|
||||
"down_mbps": 50,
|
||||
"socks5": {
|
||||
"listen": "127.0.0.1:1080"
|
||||
},
|
||||
"http": {
|
||||
"listen": "127.0.0.1:8080"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个配置同时开了 SOCK5 (支持 TCP & UDP) 代理和 HTTP 代理。Hysteria 还有很多其他模式,请务必前往 [高级用法](#高级用法) 了解一下!
|
||||
要启用/禁用一个模式,在配置文件中添加/移除对应条目即可。
|
||||
|
||||
如果你的服务端证书不是由受信任的 CA 签发的,需要用 `"ca": "/path/to/file.ca"` 指定使用的 CA 或者用 `"insecure": true` 忽略所有
|
||||
证书错误(不推荐)。
|
||||
|
||||
`up_mbps` 和 `down_mbps` 在客户端是必填选项,请根据实际网络情况尽量准确地填写,否则将影响 Hysteria 的使用体验。
|
||||
|
||||
有些用户可能会尝试用这个功能转发其他加密代理协议,比如 Shadowsocks。这样虽然可行,但从性能的角度不推荐 - Hysteria 本身就用 TLS,
|
||||
转发的代理协议也是加密的,再加上如今几乎所有网站都是 HTTPS 了,等于做了三重加密。如果需要代理,建议直接使用代理模式。
|
||||
|
||||
## 对比
|
||||
|
||||

|
||||
|
||||
## 高级用法
|
||||
|
||||
### 服务器
|
||||
|
||||
```json5
|
||||
{
|
||||
"listen": ":36712", // 监听地址
|
||||
"protocol": "faketcp", // 留空或 "udp" 为 UDP 模式,"faketcp" 为伪装 TCP 模式,详情见下
|
||||
"acme": {
|
||||
"domains": [
|
||||
"your.domain.com",
|
||||
"another.domain.net"
|
||||
], // ACME 证书域名
|
||||
"email": "hacker@gmail.com", // 注册邮箱,可选,推荐
|
||||
"disable_http": false, // 禁用 HTTP 验证方式
|
||||
"disable_tlsalpn": false, // 禁用 TLS-ALPN 验证方式
|
||||
"alt_http_port": 8080, // HTTP 验证方式替代端口
|
||||
"alt_tlsalpn_port": 4433 // TLS-ALPN 验证方式替代端口
|
||||
},
|
||||
"cert": "/home/ubuntu/my_cert.crt", // 证书
|
||||
"key": "/home/ubuntu/my_key.crt", // 证书密钥
|
||||
"up_mbps": 100, // 单客户端最大上传速度
|
||||
"down_mbps": 100, // 单客户端最大下载速度
|
||||
"disable_udp": false, // 禁用 UDP 支持
|
||||
"acl": "my_list.acl", // 见下文 ACL
|
||||
"obfs": "AMOGUS", // 混淆密码
|
||||
"auth": { // 验证
|
||||
"mode": "password", // 验证模式,暂时只支持 "password" 与 "none"
|
||||
"config": {
|
||||
"password": "yubiyubi"
|
||||
}
|
||||
},
|
||||
"alpn": "ayaya", // QUIC TLS ALPN
|
||||
"prometheus_listen": ":8080", // Prometheus 统计接口监听地址 (在 /metrics)
|
||||
"recv_window_conn": 15728640, // QUIC stream receive window
|
||||
"recv_window_client": 67108864, // QUIC connection receive window
|
||||
"max_conn_client": 4096, // 单客户端最大活跃连接数
|
||||
"disable_mtu_discovery": false, // 禁用 MTU 探测 (RFC 8899)
|
||||
"ipv6_only": false, // 强制把域名解析成 IPv6 地址
|
||||
"resolver": "1.1.1.1:53" // DNS 地址
|
||||
}
|
||||
```
|
||||
|
||||
#### ACME
|
||||
|
||||
目前仅支持 HTTP 与 TLS-ALPN 验证方式,不支持 DNS 验证。对于两种方式请分别确保 TCP 80/443 端口能够被访问。
|
||||
|
||||
#### 接入外部验证
|
||||
|
||||
如果你是商业代理服务提供商,可以这样把 Hysteria 接入到自己的验证后端:
|
||||
|
||||
```json5
|
||||
{
|
||||
// ...
|
||||
"auth": {
|
||||
"mode": "external",
|
||||
"config": {
|
||||
"http": "https://api.example.com/auth" // 支持 HTTP 和 HTTPS
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对于上述配置,Hysteria 会把验证请求通过 HTTP POST 发送到 `https://api.example.com/auth`
|
||||
|
||||
```json5
|
||||
{
|
||||
"addr": "111.222.111.222:52731",
|
||||
"payload": "[BASE64]", // 对应客户端配置的 auth 或 auth_str 字段
|
||||
"send": 12500000, // 协商后的服务端最大发送速率 (Bps)
|
||||
"recv": 12500000 // 协商后的服务端最大接收速率 (Bps)
|
||||
}
|
||||
```
|
||||
|
||||
后端必须用 HTTP 200 状态码返回验证结果(即使验证不通过):
|
||||
|
||||
```json5
|
||||
{
|
||||
"ok": false,
|
||||
"msg": "No idea who you are"
|
||||
}
|
||||
```
|
||||
|
||||
`ok` 表示验证是否通过,`msg` 是成功/失败消息。
|
||||
|
||||
#### Prometheus 流量统计
|
||||
|
||||
通过 `prometheus_listen` 选项可以让 Hysteria 暴露一个 Prometheus HTTP 客户端 endpoint 用来统计流量使用情况。
|
||||
例如如果配置在 8080 端口,则 API 地址是 `http://example.com:8080/metrics`
|
||||
|
||||
```text
|
||||
hysteria_active_conn{auth="55m95auW5oCq"} 32
|
||||
hysteria_active_conn{auth="aGFja2VyISE="} 7
|
||||
|
||||
hysteria_traffic_downlink_bytes_total{auth="55m95auW5oCq"} 122639
|
||||
hysteria_traffic_downlink_bytes_total{auth="aGFja2VyISE="} 3.225058e+06
|
||||
|
||||
hysteria_traffic_uplink_bytes_total{auth="55m95auW5oCq"} 40710
|
||||
hysteria_traffic_uplink_bytes_total{auth="aGFja2VyISE="} 37452
|
||||
```
|
||||
|
||||
`auth` 是客户端发来的验证密钥,经过 Base64 编码。
|
||||
|
||||
### 客户端
|
||||
|
||||
```json5
|
||||
{
|
||||
"server": "example.com:36712", // 服务器地址
|
||||
"protocol": "faketcp", // 留空或 "udp" 为 UDP 模式,"faketcp" 为伪装 TCP 模式,详情见下
|
||||
"up_mbps": 10, // 最大上传速度
|
||||
"down_mbps": 50, // 最大下载速度
|
||||
"socks5": {
|
||||
"listen": "127.0.0.1:1080", // SOCKS5 监听地址
|
||||
"timeout": 300, // TCP 超时秒数
|
||||
"disable_udp": false, // 禁用 UDP 支持
|
||||
"user": "me", // SOCKS5 验证用户名
|
||||
"password": "lmaolmao" // SOCKS5 验证密码
|
||||
},
|
||||
"http": {
|
||||
"listen": "127.0.0.1:8080", // HTTP 监听地址
|
||||
"timeout": 300, // TCP 超时秒数
|
||||
"user": "me", // HTTP 验证用户名
|
||||
"password": "lmaolmao", // HTTP 验证密码
|
||||
"cert": "/home/ubuntu/my_cert.crt", // 证书 (变为 HTTPS 代理)
|
||||
"key": "/home/ubuntu/my_key.crt" // 证书密钥 (变为 HTTPS 代理)
|
||||
},
|
||||
"tun": {
|
||||
"name": "tun-hy", // TUN 接口名称
|
||||
"timeout": 300, // 超时秒数
|
||||
"address": "192.0.2.2", // TUN 接口地址(不适用于 Linux)
|
||||
"gateway": "192.0.2.1", // TUN 接口网关(不适用于 Linux)
|
||||
"mask": "255.255.255.252", // TUN 接口子网掩码(不适用于 Linux)
|
||||
"dns": [ "8.8.8.8", "8.8.4.4" ], // TUN 接口 DNS 服务器(仅适用于 Windows)
|
||||
"persist": false // 在程序退出之后保留接口(仅适用于 Linux)
|
||||
},
|
||||
"relay_tcps": [
|
||||
{
|
||||
"listen": "127.0.0.1:2222", // TCP 转发监听地址
|
||||
"remote": "123.123.123.123:22", // TCP 转发目标地址
|
||||
"timeout": 300 // TCP 超时秒数
|
||||
},
|
||||
{
|
||||
"listen": "127.0.0.1:13389", // TCP 转发监听地址
|
||||
"remote": "124.124.124.124:3389", // TCP 转发目标地址
|
||||
"timeout": 300 // TCP 超时秒数
|
||||
}
|
||||
],
|
||||
"relay_udps": [
|
||||
{
|
||||
"listen": "127.0.0.1:5333", // UDP 转发监听地址
|
||||
"remote": "8.8.8.8:53", // UDP 转发目标地址
|
||||
"timeout": 60 // UDP 超时秒数
|
||||
},
|
||||
{
|
||||
"listen": "127.0.0.1:11080", // UDP 转发监听地址
|
||||
"remote": "9.9.9.9.9:1080", // UDP 转发目标地址
|
||||
"timeout": 60 // UDP 超时秒数
|
||||
}
|
||||
],
|
||||
"tproxy_tcp": {
|
||||
"listen": "127.0.0.1:9000", // TCP 透明代理监听地址
|
||||
"timeout": 300 // TCP 超时秒数
|
||||
},
|
||||
"tproxy_udp": {
|
||||
"listen": "127.0.0.1:9000", // UDP 透明代理监听地址
|
||||
"timeout": 60 // UDP 超时秒数
|
||||
},
|
||||
"acl": "my_list.acl", // 见下文 ACL
|
||||
"obfs": "AMOGUS", // 混淆密码
|
||||
"auth": "[BASE64]", // Base64 验证密钥
|
||||
"auth_str": "yubiyubi", // 字符串验证密钥,和上面的选项二选一
|
||||
"alpn": "ayaya", // QUIC TLS ALPN
|
||||
"server_name": "real.name.com", // 用于验证服务端证书的 hostname
|
||||
"insecure": false, // 忽略一切证书错误
|
||||
"ca": "my.ca", // 自定义 CA
|
||||
"recv_window_conn": 15728640, // QUIC stream receive window
|
||||
"recv_window": 67108864, // QUIC connection receive window
|
||||
"disable_mtu_discovery": false, // 禁用 MTU 探测 (RFC 8899)
|
||||
"resolver": "1.1.1.1:53" // DNS 地址
|
||||
}
|
||||
```
|
||||
|
||||
#### 伪装 TCP (faketcp 模式)
|
||||
|
||||
某些网络可能对 UDP 流量施加各种限制,或者完全屏蔽。Hysteria 提供了一个 "faketcp" 模式,让服务端与客户端之间用看起来是 TCP 但实际不走
|
||||
系统 TCP 栈的方式通信。通过这种方式可以让防火墙、QoS 设备认为这是真的 TCP 连接,绕过对 UDP 的限制。
|
||||
|
||||
目前只在 Linux 上支持(客户端和服务器都是),并且需要 root 权限。
|
||||
|
||||
如果你的服务器有防火墙,请放行相应的 TCP 端口而不是 UDP。
|
||||
|
||||
#### 透明代理
|
||||
|
||||
TPROXY 模式 (`tproxy_tcp` 和 `tproxy_udp`) 只在 Linux 下可用。
|
||||
|
||||
参考阅读:
|
||||
- https://www.kernel.org/doc/Documentation/networking/tproxy.txt
|
||||
- https://powerdns.org/tproxydoc/tproxy.md.html
|
||||
|
||||
## 优化建议
|
||||
|
||||
### 针对超高传速度进行优化
|
||||
|
||||
如果要用 Hysteria 进行极高速度的传输 (如内网超过 10G 或高延迟跨国超过 1G),请增加系统的 UDP receive buffer 大小。
|
||||
|
||||
```shell
|
||||
sysctl -w net.core.rmem_max=4000000
|
||||
```
|
||||
|
||||
这个命令会在 Linux 下将 buffer 大小提升到 4 MB 左右。
|
||||
|
||||
你可能还需要提高 `recv_window_conn` 和 `recv_window` (服务器端是 `recv_window_client`) 以确保它们至少不低于带宽-延迟的乘积。
|
||||
比如如果想在一条 RTT 200ms 的线路上达到 500 MB/s 的速度,receive window 至少需要 100 MB (500*0.2)
|
||||
|
||||
### 路由器与其他嵌入式设备
|
||||
|
||||
对于运算性能和内存十分有限的嵌入式设备,如果不是必须的话建议关闭混淆,可以带来少许性能提升。
|
||||
|
||||
Hysteria 服务端与客户端默认的 receive window 大小是 64 MB。如果设备内存不够,请考虑通过配置降低。建议保持 stream receive window
|
||||
和 connection receive window 之间 1:4 的比例关系。
|
||||
|
||||
## 关于 ACL
|
||||
|
||||
[ACL 文件格式](ACL.zh.md)
|
||||
|
||||
ACL 在服务端和客户端都可以使用。在服务端可以用来实现限制客户端能访问的目标,对客户端任何模式都有效。在客户端只有 SOCKS5 和 HTTP 代理
|
||||
支持 ACL。其他模式下没有效果(所有流量都会走代理)。
|
||||
|
||||
## URI Scheme
|
||||
|
||||
希望包含链接分享/导入功能的第三方客户端,建议按照如下 URI Scheme 实现(最初由 Shadowrocket 引入):
|
||||
|
||||
hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks
|
||||
|
||||
- host: hostname or IP address of the server to connect to (required)
|
||||
- port: port of the server to connect to (required)
|
||||
- protocol: protocol to use ("udp" or "faketcp") (optional, default: "udp")
|
||||
- auth: authentication payload (string) (optional)
|
||||
- peer: SNI for TLS (optional)
|
||||
- insecure: ignore certificate errors (optional)
|
||||
- upmbps: upstream bandwidth in Mbps (required)
|
||||
- downmbps: downstream bandwidth in Mbps (required)
|
||||
- alpn: QUIC ALPN (optional)
|
||||
- obfs: Obfuscation mode (optional, empty or "xplus")
|
||||
- obfsParam: Obfuscation password (optional)
|
||||
- remarks: remarks (optional)
|
||||
|
||||
## 日志
|
||||
|
||||
程序默认在 stdout 输出 DEBUG 级别,文字格式的日志。
|
||||
|
||||
如果需要修改日志级别可以使用 `LOGGING_LEVEL` 环境变量,支持 `panic`, `fatal`, `error`, `warn`, `info`, `debug`, `trace`
|
||||
|
||||
如果需要输出 JSON 可以把 `LOGGING_FORMATTER` 设置为 `json`
|
||||
|
||||
如果需要修改日志时间戳格式可以使用 `LOGGING_TIMESTAMP_FORMAT`
|
||||
|
||||
|
||||
## 自定义 CA 方法
|
||||
|
||||
1. 假设服务器地址是 `123.123.123.123`, 端口`5678`UDP/TCP协议未被防火墙拦截
|
||||
2. 已经安装了 openssl
|
||||
3. hysteria 已经安装在 `/root/hysteria/`目录下
|
||||
<details>
|
||||
<summary>4. 生成自定义CA证书</summary>
|
||||
|
||||
- 在 `/root/hysteria/` 目录下,将以下shell命令保存为 `generate.sh` , 并赋予执行权限: `chmod +x ./generate.sh` 后,运行 `./generate.sh` 命令生成自定义CA证书
|
||||
- 或者在`/root/hysteria/` 目录下,直接执行以下shell命令生成自定义CA证书
|
||||
|
||||
``` shell
|
||||
#!/usr/bin/env bash
|
||||
|
||||
domain=$(openssl rand -hex 8)
|
||||
password=$(openssl rand -hex 16)
|
||||
obfs=$(openssl rand -hex 6)
|
||||
path="/root/hysteria"
|
||||
# 生成CAkey
|
||||
openssl genrsa -out hysteria.ca.key 2048
|
||||
# 生成CA证书
|
||||
openssl req -new -x509 -days 3650 -key hysteria.ca.key -subj "/C=CN/ST=GD/L=SZ/O=Hysteria, Inc./CN=Hysteria Root CA" -out hysteria.ca.crt
|
||||
|
||||
openssl req -newkey rsa:2048 -nodes -keyout hysteria.server.key -subj "/C=CN/ST=GD/L=SZ/O=Hysteria, Inc./CN=*.${domain}.com" -out hysteria.server.csr
|
||||
# 签发服务端用的证书
|
||||
openssl x509 -req -extfile <(printf "subjectAltName=DNS:${domain}.com,DNS:www.${domain}.com") -days 3650 -in hysteria.server.csr -CA hysteria.ca.crt -CAkey hysteria.ca.key -CAcreateserial -out hysteria.server.crt
|
||||
|
||||
cat > ./client.json <<EOF
|
||||
{
|
||||
"server": "123.123.123.123:5678",
|
||||
"alpn": "h3",
|
||||
"obfs": "${obfs}",
|
||||
"auth_str": "${password}",
|
||||
"up_mbps": 30,
|
||||
"down_mbps": 30,
|
||||
"socks5": {
|
||||
"listen": "0.0.0.0:1080"
|
||||
},
|
||||
"http": {
|
||||
"listen": "0.0.0.0:8080"
|
||||
},
|
||||
"server_name": "www.${domain}.com",
|
||||
"ca": "${path}/hysteria.ca.crt"
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
cat > ./server.json <<EOF
|
||||
{
|
||||
"listen": ":5678",
|
||||
"alpn": "h3",
|
||||
"obfs": "${obfs}",
|
||||
"cert": "${path}/hysteria.server.crt",
|
||||
"key": "${path}/hysteria.server.key" ,
|
||||
"auth": {
|
||||
"mode": "password",
|
||||
"config": {
|
||||
"password": "${password}"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
</details>
|
||||
|
||||
5. 服务端:复制 `server.json`、 `hysteria.server.crt`、 `hysteria.server.key` 到 `/root/hysteria/` 目录下,运行 `/root/hysteria/hysteria -c /root/hysteria/server.json server` 命令
|
||||
|
||||
6. 客户端:假设客户端运行目录也为`/root/hysteria`, 复制 `client.json`、`hysteria.ca.crt` 到 `/root/hysteria/` 目录下,运行 `/root/hysteria/hysteria -c /root/hysteria/client.json` 命令
|
||||
|
||||
7. 生成CA证书之后,根据自身情况修改服务器地址、端口和证书文件路径,加上`obfs`和`alpn`是防止首次在某些环境下被墙,第一次在全参数情况下测试通过后,可以自身网络环境删除不必须要参数,比如`obfs`和`alpn`.
|
||||
|
||||
8. IOS端如果使用的是小火箭shadowrocket,可以把文件`hysteria.ca.crt` airdrop到手机,然后在手机上安装并信任后, 就可以使用自定义CA证书了。
|
7
app/LICENSE.md
Normal file
7
app/LICENSE.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
Copyright 2023 Toby
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
1033
app/cmd/client.go
Normal file
1033
app/cmd/client.go
Normal file
File diff suppressed because it is too large
Load diff
204
app/cmd/client_test.go
Normal file
204
app/cmd/client_test.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// TestClientConfig tests the parsing of the client config
|
||||
func TestClientConfig(t *testing.T) {
|
||||
viper.SetConfigFile("client_test.yaml")
|
||||
err := viper.ReadInConfig()
|
||||
assert.NoError(t, err)
|
||||
var config clientConfig
|
||||
err = viper.Unmarshal(&config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, config, clientConfig{
|
||||
Server: "example.com",
|
||||
Auth: "weak_ahh_password",
|
||||
Transport: clientConfigTransport{
|
||||
Type: "udp",
|
||||
UDP: clientConfigTransportUDP{
|
||||
HopInterval: 30 * time.Second,
|
||||
},
|
||||
},
|
||||
Obfs: clientConfigObfs{
|
||||
Type: "salamander",
|
||||
Salamander: clientConfigObfsSalamander{
|
||||
Password: "cry_me_a_r1ver",
|
||||
},
|
||||
},
|
||||
TLS: clientConfigTLS{
|
||||
SNI: "another.example.com",
|
||||
Insecure: true,
|
||||
PinSHA256: "114515DEADBEEF",
|
||||
CA: "custom_ca.crt",
|
||||
},
|
||||
QUIC: clientConfigQUIC{
|
||||
InitStreamReceiveWindow: 1145141,
|
||||
MaxStreamReceiveWindow: 1145142,
|
||||
InitConnectionReceiveWindow: 1145143,
|
||||
MaxConnectionReceiveWindow: 1145144,
|
||||
MaxIdleTimeout: 10 * time.Second,
|
||||
KeepAlivePeriod: 4 * time.Second,
|
||||
DisablePathMTUDiscovery: true,
|
||||
Sockopts: clientConfigQUICSockopts{
|
||||
BindInterface: stringRef("eth0"),
|
||||
FirewallMark: uint32Ref(1234),
|
||||
FdControlUnixSocket: stringRef("test.sock"),
|
||||
},
|
||||
},
|
||||
Bandwidth: clientConfigBandwidth{
|
||||
Up: "200 mbps",
|
||||
Down: "1 gbps",
|
||||
},
|
||||
FastOpen: true,
|
||||
Lazy: true,
|
||||
SOCKS5: &socks5Config{
|
||||
Listen: "127.0.0.1:1080",
|
||||
Username: "anon",
|
||||
Password: "bro",
|
||||
DisableUDP: true,
|
||||
},
|
||||
HTTP: &httpConfig{
|
||||
Listen: "127.0.0.1:8080",
|
||||
Username: "qqq",
|
||||
Password: "bruh",
|
||||
Realm: "martian",
|
||||
},
|
||||
TCPForwarding: []tcpForwardingEntry{
|
||||
{
|
||||
Listen: "127.0.0.1:8088",
|
||||
Remote: "internal.example.com:80",
|
||||
},
|
||||
},
|
||||
UDPForwarding: []udpForwardingEntry{
|
||||
{
|
||||
Listen: "127.0.0.1:5353",
|
||||
Remote: "internal.example.com:53",
|
||||
Timeout: 50 * time.Second,
|
||||
},
|
||||
},
|
||||
TCPTProxy: &tcpTProxyConfig{
|
||||
Listen: "127.0.0.1:2500",
|
||||
},
|
||||
UDPTProxy: &udpTProxyConfig{
|
||||
Listen: "127.0.0.1:2501",
|
||||
Timeout: 20 * time.Second,
|
||||
},
|
||||
TCPRedirect: &tcpRedirectConfig{
|
||||
Listen: "127.0.0.1:3500",
|
||||
},
|
||||
TUN: &tunConfig{
|
||||
Name: "hytun",
|
||||
MTU: 1500,
|
||||
Timeout: 60 * time.Second,
|
||||
Address: struct {
|
||||
IPv4 string `mapstructure:"ipv4"`
|
||||
IPv6 string `mapstructure:"ipv6"`
|
||||
}{IPv4: "100.100.100.101/30", IPv6: "2001::ffff:ffff:ffff:fff1/126"},
|
||||
Route: &struct {
|
||||
Strict bool `mapstructure:"strict"`
|
||||
IPv4 []string `mapstructure:"ipv4"`
|
||||
IPv6 []string `mapstructure:"ipv6"`
|
||||
IPv4Exclude []string `mapstructure:"ipv4Exclude"`
|
||||
IPv6Exclude []string `mapstructure:"ipv6Exclude"`
|
||||
}{
|
||||
Strict: true,
|
||||
IPv4: []string{"0.0.0.0/0"},
|
||||
IPv6: []string{"2000::/3"},
|
||||
IPv4Exclude: []string{"192.0.2.1/32"},
|
||||
IPv6Exclude: []string{"2001:db8::1/128"},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TestClientConfigURI tests URI-related functions of clientConfig
|
||||
func TestClientConfigURI(t *testing.T) {
|
||||
tests := []struct {
|
||||
uri string
|
||||
uriOK bool
|
||||
config *clientConfig
|
||||
}{
|
||||
{
|
||||
uri: "hysteria2://god@zilla.jp/",
|
||||
uriOK: true,
|
||||
config: &clientConfig{
|
||||
Server: "zilla.jp",
|
||||
Auth: "god",
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: "hysteria2://john:wick@continental.org:4443/",
|
||||
uriOK: true,
|
||||
config: &clientConfig{
|
||||
Server: "continental.org:4443",
|
||||
Auth: "john:wick",
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: "hysteria2://saul@better.call:7000-10000,20000/",
|
||||
uriOK: true,
|
||||
config: &clientConfig{
|
||||
Server: "better.call:7000-10000,20000",
|
||||
Auth: "saul",
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: "hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&pinSHA256=deadbeef&sni=crap.cc",
|
||||
uriOK: true,
|
||||
config: &clientConfig{
|
||||
Server: "noauth.com",
|
||||
Auth: "",
|
||||
Obfs: clientConfigObfs{
|
||||
Type: "salamander",
|
||||
Salamander: clientConfigObfsSalamander{
|
||||
Password: "66ccff",
|
||||
},
|
||||
},
|
||||
TLS: clientConfigTLS{
|
||||
SNI: "crap.cc",
|
||||
Insecure: true,
|
||||
PinSHA256: "deadbeef",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: "invalid.bs",
|
||||
uriOK: false,
|
||||
config: nil,
|
||||
},
|
||||
{
|
||||
uri: "https://www.google.com/search?q=test",
|
||||
uriOK: false,
|
||||
config: nil,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.uri, func(t *testing.T) {
|
||||
// Test parseURI
|
||||
nc := &clientConfig{Server: test.uri}
|
||||
assert.Equal(t, nc.parseURI(), test.uriOK)
|
||||
if test.uriOK {
|
||||
assert.Equal(t, nc, test.config)
|
||||
}
|
||||
// Test URI generation
|
||||
if test.config != nil {
|
||||
assert.Equal(t, test.config.URI(), test.uri)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stringRef(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func uint32Ref(i uint32) *uint32 {
|
||||
return &i
|
||||
}
|
85
app/cmd/client_test.yaml
Normal file
85
app/cmd/client_test.yaml
Normal file
|
@ -0,0 +1,85 @@
|
|||
server: example.com
|
||||
|
||||
auth: weak_ahh_password
|
||||
|
||||
transport:
|
||||
type: udp
|
||||
udp:
|
||||
hopInterval: 30s
|
||||
|
||||
obfs:
|
||||
type: salamander
|
||||
salamander:
|
||||
password: cry_me_a_r1ver
|
||||
|
||||
tls:
|
||||
sni: another.example.com
|
||||
insecure: true
|
||||
pinSHA256: 114515DEADBEEF
|
||||
ca: custom_ca.crt
|
||||
|
||||
quic:
|
||||
initStreamReceiveWindow: 1145141
|
||||
maxStreamReceiveWindow: 1145142
|
||||
initConnReceiveWindow: 1145143
|
||||
maxConnReceiveWindow: 1145144
|
||||
maxIdleTimeout: 10s
|
||||
keepAlivePeriod: 4s
|
||||
disablePathMTUDiscovery: true
|
||||
sockopts:
|
||||
bindInterface: eth0
|
||||
fwmark: 1234
|
||||
fdControlUnixSocket: test.sock
|
||||
|
||||
bandwidth:
|
||||
up: 200 mbps
|
||||
down: 1 gbps
|
||||
|
||||
fastOpen: true
|
||||
|
||||
lazy: true
|
||||
|
||||
socks5:
|
||||
listen: 127.0.0.1:1080
|
||||
username: anon
|
||||
password: bro
|
||||
disableUDP: true
|
||||
|
||||
http:
|
||||
listen: 127.0.0.1:8080
|
||||
username: qqq
|
||||
password: bruh
|
||||
realm: martian
|
||||
|
||||
tcpForwarding:
|
||||
- listen: 127.0.0.1:8088
|
||||
remote: internal.example.com:80
|
||||
|
||||
udpForwarding:
|
||||
- listen: 127.0.0.1:5353
|
||||
remote: internal.example.com:53
|
||||
timeout: 50s
|
||||
|
||||
tcpTProxy:
|
||||
listen: 127.0.0.1:2500
|
||||
|
||||
udpTProxy:
|
||||
listen: 127.0.0.1:2501
|
||||
timeout: 20s
|
||||
|
||||
tcpRedirect:
|
||||
listen: 127.0.0.1:3500
|
||||
|
||||
tun:
|
||||
name: "hytun"
|
||||
mtu: 1500
|
||||
timeout: 1m
|
||||
address:
|
||||
ipv4: 100.100.100.101/30
|
||||
ipv6: 2001::ffff:ffff:ffff:fff1/126
|
||||
route:
|
||||
strict: true
|
||||
ipv4: [ 0.0.0.0/0 ]
|
||||
ipv6: [ "2000::/3" ]
|
||||
ipv4Exclude: [ 192.0.2.1/32 ]
|
||||
ipv6Exclude: [ "2001:db8::1/128" ]
|
18
app/cmd/errors.go
Normal file
18
app/cmd/errors.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type configError struct {
|
||||
Field string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e configError) Error() string {
|
||||
return fmt.Sprintf("invalid config: %s: %s", e.Field, e.Err)
|
||||
}
|
||||
|
||||
func (e configError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
63
app/cmd/ping.go
Normal file
63
app/cmd/ping.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
// pingCmd represents the ping command
|
||||
var pingCmd = &cobra.Command{
|
||||
Use: "ping address",
|
||||
Short: "Ping mode",
|
||||
Long: "Perform a TCP ping to a specified remote address through the proxy server. Can be used as a simple connectivity test.",
|
||||
Run: runPing,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(pingCmd)
|
||||
}
|
||||
|
||||
func runPing(cmd *cobra.Command, args []string) {
|
||||
logger.Info("ping mode")
|
||||
|
||||
if len(args) != 1 {
|
||||
logger.Fatal("must specify one and only one address")
|
||||
}
|
||||
addr := args[0]
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
logger.Fatal("failed to read client config", zap.Error(err))
|
||||
}
|
||||
var config clientConfig
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
logger.Fatal("failed to parse client config", zap.Error(err))
|
||||
}
|
||||
hyConfig, err := config.Config()
|
||||
if err != nil {
|
||||
logger.Fatal("failed to load client config", zap.Error(err))
|
||||
}
|
||||
|
||||
c, info, err := client.NewClient(hyConfig)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to initialize client", zap.Error(err))
|
||||
}
|
||||
defer c.Close()
|
||||
logger.Info("connected to server",
|
||||
zap.Bool("udpEnabled", info.UDPEnabled),
|
||||
zap.Uint64("tx", info.Tx))
|
||||
|
||||
logger.Info("connecting", zap.String("addr", addr))
|
||||
start := time.Now()
|
||||
conn, err := c.TCP(addr)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to connect", zap.Error(err), zap.String("time", time.Since(start).String()))
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
logger.Info("connected", zap.String("time", time.Since(start).String()))
|
||||
}
|
176
app/cmd/root.go
Normal file
176
app/cmd/root.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
const (
|
||||
appLogo = `
|
||||
░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█░░░▀▀▄
|
||||
░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█░░░▄▀░
|
||||
░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀▀▀
|
||||
`
|
||||
appDesc = "a powerful, lightning fast and censorship resistant proxy"
|
||||
appAuthors = "Aperture Internet Laboratory <https://github.com/apernet>"
|
||||
|
||||
appLogLevelEnv = "HYSTERIA_LOG_LEVEL"
|
||||
appLogFormatEnv = "HYSTERIA_LOG_FORMAT"
|
||||
appDisableUpdateCheckEnv = "HYSTERIA_DISABLE_UPDATE_CHECK"
|
||||
appACMEDirEnv = "HYSTERIA_ACME_DIR"
|
||||
)
|
||||
|
||||
var (
|
||||
// These values will be injected by the build system
|
||||
appVersion = "Unknown"
|
||||
appDate = "Unknown"
|
||||
appType = "Unknown" // aka channel
|
||||
appToolchain = "Unknown"
|
||||
appCommit = "Unknown"
|
||||
appPlatform = "Unknown"
|
||||
appArch = "Unknown"
|
||||
libVersion = "Unknown"
|
||||
|
||||
appVersionLong = fmt.Sprintf("Version:\t%s\n"+
|
||||
"BuildDate:\t%s\n"+
|
||||
"BuildType:\t%s\n"+
|
||||
"Toolchain:\t%s\n"+
|
||||
"CommitHash:\t%s\n"+
|
||||
"Platform:\t%s\n"+
|
||||
"Architecture:\t%s\n"+
|
||||
"Libraries:\tquic-go=%s",
|
||||
appVersion, appDate, appType, appToolchain, appCommit, appPlatform, appArch, libVersion)
|
||||
|
||||
appAboutLong = fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong)
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
|
||||
// Flags
|
||||
var (
|
||||
cfgFile string
|
||||
logLevel string
|
||||
logFormat string
|
||||
disableUpdateCheck bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "hysteria",
|
||||
Short: appDesc,
|
||||
Long: appAboutLong,
|
||||
Run: runClient, // Default to client mode
|
||||
}
|
||||
|
||||
var logLevelMap = map[string]zapcore.Level{
|
||||
"debug": zapcore.DebugLevel,
|
||||
"info": zapcore.InfoLevel,
|
||||
"warn": zapcore.WarnLevel,
|
||||
"error": zapcore.ErrorLevel,
|
||||
}
|
||||
|
||||
var logFormatMap = map[string]zapcore.EncoderConfig{
|
||||
"console": {
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
MessageKey: "msg",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.CapitalColorLevelEncoder,
|
||||
EncodeTime: zapcore.RFC3339TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
},
|
||||
"json": {
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
MessageKey: "msg",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: zapcore.EpochMillisTimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
initFlags()
|
||||
cobra.MousetrapHelpText = "" // Disable the mousetrap so Windows users can run the exe directly by double-clicking
|
||||
cobra.OnInitialize(initConfig)
|
||||
cobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config
|
||||
}
|
||||
|
||||
func initFlags() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
|
||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", envOrDefaultString(appLogLevelEnv, "info"), "log level")
|
||||
rootCmd.PersistentFlags().StringVarP(&logFormat, "log-format", "f", envOrDefaultString(appLogFormatEnv, "console"), "log format")
|
||||
rootCmd.PersistentFlags().BoolVar(&disableUpdateCheck, "disable-update-check", envOrDefaultBool(appDisableUpdateCheckEnv, false), "disable update check")
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SupportedExts = append([]string{"yaml", "yml"}, viper.SupportedExts...)
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("$HOME/.hysteria")
|
||||
viper.AddConfigPath("/etc/hysteria/")
|
||||
}
|
||||
}
|
||||
|
||||
func initLogger() {
|
||||
level, ok := logLevelMap[strings.ToLower(logLevel)]
|
||||
if !ok {
|
||||
fmt.Printf("unsupported log level: %s\n", logLevel)
|
||||
os.Exit(1)
|
||||
}
|
||||
enc, ok := logFormatMap[strings.ToLower(logFormat)]
|
||||
if !ok {
|
||||
fmt.Printf("unsupported log format: %s\n", logFormat)
|
||||
os.Exit(1)
|
||||
}
|
||||
c := zap.Config{
|
||||
Level: zap.NewAtomicLevelAt(level),
|
||||
DisableCaller: true,
|
||||
DisableStacktrace: true,
|
||||
Encoding: strings.ToLower(logFormat),
|
||||
EncoderConfig: enc,
|
||||
OutputPaths: []string{"stderr"},
|
||||
ErrorOutputPaths: []string{"stderr"},
|
||||
}
|
||||
var err error
|
||||
logger, err = c.Build()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to initialize logger: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func envOrDefaultString(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func envOrDefaultBool(key string, def bool) bool {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
b, _ := strconv.ParseBool(v)
|
||||
return b
|
||||
}
|
||||
return def
|
||||
}
|
1051
app/cmd/server.go
Normal file
1051
app/cmd/server.go
Normal file
File diff suppressed because it is too large
Load diff
189
app/cmd/server_test.go
Normal file
189
app/cmd/server_test.go
Normal file
|
@ -0,0 +1,189 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// TestServerConfig tests the parsing of the server config
|
||||
func TestServerConfig(t *testing.T) {
|
||||
viper.SetConfigFile("server_test.yaml")
|
||||
err := viper.ReadInConfig()
|
||||
assert.NoError(t, err)
|
||||
var config serverConfig
|
||||
err = viper.Unmarshal(&config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, config, serverConfig{
|
||||
Listen: ":8443",
|
||||
Obfs: serverConfigObfs{
|
||||
Type: "salamander",
|
||||
Salamander: serverConfigObfsSalamander{
|
||||
Password: "cry_me_a_r1ver",
|
||||
},
|
||||
},
|
||||
TLS: &serverConfigTLS{
|
||||
Cert: "some.crt",
|
||||
Key: "some.key",
|
||||
SNIGuard: "strict",
|
||||
},
|
||||
ACME: &serverConfigACME{
|
||||
Domains: []string{
|
||||
"sub1.example.com",
|
||||
"sub2.example.com",
|
||||
},
|
||||
Email: "haha@cringe.net",
|
||||
CA: "zero",
|
||||
ListenHost: "127.0.0.9",
|
||||
Dir: "random_dir",
|
||||
Type: "dns",
|
||||
HTTP: serverConfigACMEHTTP{
|
||||
AltPort: 8888,
|
||||
},
|
||||
TLS: serverConfigACMETLS{
|
||||
AltPort: 44333,
|
||||
},
|
||||
DNS: serverConfigACMEDNS{
|
||||
Name: "gomommy",
|
||||
Config: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
DisableHTTP: true,
|
||||
DisableTLSALPN: true,
|
||||
AltHTTPPort: 8080,
|
||||
AltTLSALPNPort: 4433,
|
||||
},
|
||||
QUIC: serverConfigQUIC{
|
||||
InitStreamReceiveWindow: 77881,
|
||||
MaxStreamReceiveWindow: 77882,
|
||||
InitConnectionReceiveWindow: 77883,
|
||||
MaxConnectionReceiveWindow: 77884,
|
||||
MaxIdleTimeout: 999 * time.Second,
|
||||
MaxIncomingStreams: 256,
|
||||
DisablePathMTUDiscovery: true,
|
||||
},
|
||||
Bandwidth: serverConfigBandwidth{
|
||||
Up: "500 mbps",
|
||||
Down: "100 mbps",
|
||||
},
|
||||
IgnoreClientBandwidth: true,
|
||||
SpeedTest: true,
|
||||
DisableUDP: true,
|
||||
UDPIdleTimeout: 120 * time.Second,
|
||||
Auth: serverConfigAuth{
|
||||
Type: "password",
|
||||
Password: "goofy_ahh_password",
|
||||
UserPass: map[string]string{
|
||||
"yolo": "swag",
|
||||
"lol": "kek",
|
||||
"foo": "bar",
|
||||
},
|
||||
HTTP: serverConfigAuthHTTP{
|
||||
URL: "http://127.0.0.1:5000/auth",
|
||||
Insecure: true,
|
||||
},
|
||||
Command: "/etc/some_command",
|
||||
},
|
||||
Resolver: serverConfigResolver{
|
||||
Type: "udp",
|
||||
TCP: serverConfigResolverTCP{
|
||||
Addr: "123.123.123.123:5353",
|
||||
Timeout: 4 * time.Second,
|
||||
},
|
||||
UDP: serverConfigResolverUDP{
|
||||
Addr: "4.6.8.0:53",
|
||||
Timeout: 2 * time.Second,
|
||||
},
|
||||
TLS: serverConfigResolverTLS{
|
||||
Addr: "dot.yolo.com:8853",
|
||||
Timeout: 10 * time.Second,
|
||||
SNI: "server1.yolo.net",
|
||||
Insecure: true,
|
||||
},
|
||||
HTTPS: serverConfigResolverHTTPS{
|
||||
Addr: "cringe.ahh.cc",
|
||||
Timeout: 5 * time.Second,
|
||||
SNI: "real.stuff.net",
|
||||
Insecure: true,
|
||||
},
|
||||
},
|
||||
Sniff: serverConfigSniff{
|
||||
Enable: true,
|
||||
Timeout: 1 * time.Second,
|
||||
RewriteDomain: true,
|
||||
TCPPorts: "80,443,1000-2000",
|
||||
UDPPorts: "443",
|
||||
},
|
||||
ACL: serverConfigACL{
|
||||
File: "chnroute.txt",
|
||||
Inline: []string{
|
||||
"lmao(ok)",
|
||||
"kek(cringe,boba,tea)",
|
||||
},
|
||||
GeoIP: "some.dat",
|
||||
GeoSite: "some_site.dat",
|
||||
GeoUpdateInterval: 168 * time.Hour,
|
||||
},
|
||||
Outbounds: []serverConfigOutboundEntry{
|
||||
{
|
||||
Name: "goodstuff",
|
||||
Type: "direct",
|
||||
Direct: serverConfigOutboundDirect{
|
||||
Mode: "64",
|
||||
BindIPv4: "2.4.6.8",
|
||||
BindIPv6: "0:0:0:0:0:ffff:0204:0608",
|
||||
BindDevice: "eth233",
|
||||
FastOpen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "badstuff",
|
||||
Type: "socks5",
|
||||
SOCKS5: serverConfigOutboundSOCKS5{
|
||||
Addr: "shady.proxy.ru:1080",
|
||||
Username: "hackerman",
|
||||
Password: "Elliot Alderson",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "weirdstuff",
|
||||
Type: "http",
|
||||
HTTP: serverConfigOutboundHTTP{
|
||||
URL: "https://eyy.lmao:4443/goofy",
|
||||
Insecure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
TrafficStats: serverConfigTrafficStats{
|
||||
Listen: ":9999",
|
||||
Secret: "its_me_mario",
|
||||
},
|
||||
Masquerade: serverConfigMasquerade{
|
||||
Type: "proxy",
|
||||
File: serverConfigMasqueradeFile{
|
||||
Dir: "/www/masq",
|
||||
},
|
||||
Proxy: serverConfigMasqueradeProxy{
|
||||
URL: "https://some.site.net",
|
||||
RewriteHost: true,
|
||||
Insecure: true,
|
||||
},
|
||||
String: serverConfigMasqueradeString{
|
||||
Content: "aint nothin here",
|
||||
Headers: map[string]string{
|
||||
"content-type": "text/plain",
|
||||
"custom-haha": "lol",
|
||||
},
|
||||
StatusCode: 418,
|
||||
},
|
||||
ListenHTTP: ":80",
|
||||
ListenHTTPS: ":443",
|
||||
ForceHTTPS: true,
|
||||
},
|
||||
})
|
||||
}
|
144
app/cmd/server_test.yaml
Normal file
144
app/cmd/server_test.yaml
Normal file
|
@ -0,0 +1,144 @@
|
|||
listen: :8443
|
||||
|
||||
obfs:
|
||||
type: salamander
|
||||
salamander:
|
||||
password: cry_me_a_r1ver
|
||||
|
||||
tls:
|
||||
cert: some.crt
|
||||
key: some.key
|
||||
sniGuard: strict
|
||||
|
||||
acme:
|
||||
domains:
|
||||
- sub1.example.com
|
||||
- sub2.example.com
|
||||
email: haha@cringe.net
|
||||
ca: zero
|
||||
listenHost: 127.0.0.9
|
||||
dir: random_dir
|
||||
type: dns
|
||||
http:
|
||||
altPort: 8888
|
||||
tls:
|
||||
altPort: 44333
|
||||
dns:
|
||||
name: gomommy
|
||||
config:
|
||||
key1: value1
|
||||
key2: value2
|
||||
disableHTTP: true
|
||||
disableTLSALPN: true
|
||||
altHTTPPort: 8080
|
||||
altTLSALPNPort: 4433
|
||||
|
||||
quic:
|
||||
initStreamReceiveWindow: 77881
|
||||
maxStreamReceiveWindow: 77882
|
||||
initConnReceiveWindow: 77883
|
||||
maxConnReceiveWindow: 77884
|
||||
maxIdleTimeout: 999s
|
||||
maxIncomingStreams: 256
|
||||
disablePathMTUDiscovery: true
|
||||
|
||||
bandwidth:
|
||||
up: 500 mbps
|
||||
down: 100 mbps
|
||||
|
||||
ignoreClientBandwidth: true
|
||||
|
||||
speedTest: true
|
||||
|
||||
disableUDP: true
|
||||
udpIdleTimeout: 120s
|
||||
|
||||
auth:
|
||||
type: password
|
||||
password: goofy_ahh_password
|
||||
userpass:
|
||||
yolo: swag
|
||||
lol: kek
|
||||
foo: bar
|
||||
http:
|
||||
url: http://127.0.0.1:5000/auth
|
||||
insecure: true
|
||||
command: /etc/some_command
|
||||
|
||||
resolver:
|
||||
type: udp
|
||||
tcp:
|
||||
addr: 123.123.123.123:5353
|
||||
timeout: 4s
|
||||
udp:
|
||||
addr: 4.6.8.0:53
|
||||
timeout: 2s
|
||||
tls:
|
||||
addr: dot.yolo.com:8853
|
||||
timeout: 10s
|
||||
sni: server1.yolo.net
|
||||
insecure: true
|
||||
https:
|
||||
addr: cringe.ahh.cc
|
||||
timeout: 5s
|
||||
sni: real.stuff.net
|
||||
insecure: true
|
||||
|
||||
sniff:
|
||||
enable: true
|
||||
timeout: 1s
|
||||
rewriteDomain: true
|
||||
tcpPorts: 80,443,1000-2000
|
||||
udpPorts: 443
|
||||
|
||||
acl:
|
||||
file: chnroute.txt
|
||||
inline:
|
||||
- lmao(ok)
|
||||
- kek(cringe,boba,tea)
|
||||
geoip: some.dat
|
||||
geosite: some_site.dat
|
||||
geoUpdateInterval: 168h
|
||||
|
||||
outbounds:
|
||||
- name: goodstuff
|
||||
type: direct
|
||||
direct:
|
||||
mode: 64
|
||||
bindIPv4: 2.4.6.8
|
||||
bindIPv6: 0:0:0:0:0:ffff:0204:0608
|
||||
bindDevice: eth233
|
||||
fastOpen: true
|
||||
- name: badstuff
|
||||
type: socks5
|
||||
socks5:
|
||||
addr: shady.proxy.ru:1080
|
||||
username: hackerman
|
||||
password: Elliot Alderson
|
||||
- name: weirdstuff
|
||||
type: http
|
||||
http:
|
||||
url: https://eyy.lmao:4443/goofy
|
||||
insecure: true
|
||||
|
||||
trafficStats:
|
||||
listen: :9999
|
||||
secret: its_me_mario
|
||||
|
||||
masquerade:
|
||||
type: proxy
|
||||
file:
|
||||
dir: /www/masq
|
||||
proxy:
|
||||
url: https://some.site.net
|
||||
rewriteHost: true
|
||||
insecure: true
|
||||
string:
|
||||
content: aint nothin here
|
||||
headers:
|
||||
content-type: text/plain
|
||||
custom-haha: lol
|
||||
statusCode: 418
|
||||
listenHTTP: :80
|
||||
listenHTTPS: :443
|
||||
forceHTTPS: true
|
55
app/cmd/share.go
Normal file
55
app/cmd/share.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/apernet/hysteria/app/v2/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
noText bool
|
||||
withQR bool
|
||||
)
|
||||
|
||||
// shareCmd represents the share command
|
||||
var shareCmd = &cobra.Command{
|
||||
Use: "share",
|
||||
Short: "Generate share URI",
|
||||
Long: "Generate a hysteria2:// URI from a client config for sharing",
|
||||
Run: runShare,
|
||||
}
|
||||
|
||||
func init() {
|
||||
initShareFlags()
|
||||
rootCmd.AddCommand(shareCmd)
|
||||
}
|
||||
|
||||
func initShareFlags() {
|
||||
shareCmd.Flags().BoolVar(&noText, "notext", false, "do not show URI as text")
|
||||
shareCmd.Flags().BoolVar(&withQR, "qr", false, "show URI as QR code")
|
||||
}
|
||||
|
||||
func runShare(cmd *cobra.Command, args []string) {
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
logger.Fatal("failed to read client config", zap.Error(err))
|
||||
}
|
||||
var config clientConfig
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
logger.Fatal("failed to parse client config", zap.Error(err))
|
||||
}
|
||||
if _, err := config.Config(); err != nil {
|
||||
logger.Fatal("failed to load client config", zap.Error(err))
|
||||
}
|
||||
|
||||
u := config.URI()
|
||||
|
||||
if !noText {
|
||||
fmt.Println(u)
|
||||
}
|
||||
if withQR {
|
||||
utils.PrintQR(u)
|
||||
}
|
||||
}
|
178
app/cmd/speedtest.go
Normal file
178
app/cmd/speedtest.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
hyErrors "github.com/apernet/hysteria/core/v2/errors"
|
||||
"github.com/apernet/hysteria/extras/v2/outbounds"
|
||||
"github.com/apernet/hysteria/extras/v2/outbounds/speedtest"
|
||||
)
|
||||
|
||||
var (
|
||||
skipDownload bool
|
||||
skipUpload bool
|
||||
dataSize uint32
|
||||
useBytes bool
|
||||
|
||||
speedtestAddr = fmt.Sprintf("%s:%d", outbounds.SpeedtestDest, 0)
|
||||
)
|
||||
|
||||
// speedtestCmd represents the speedtest command
|
||||
var speedtestCmd = &cobra.Command{
|
||||
Use: "speedtest",
|
||||
Short: "Speed test mode",
|
||||
Long: "Perform a speed test through the proxy server. The server must have speed test support enabled.",
|
||||
Run: runSpeedtest,
|
||||
}
|
||||
|
||||
func init() {
|
||||
initSpeedtestFlags()
|
||||
rootCmd.AddCommand(speedtestCmd)
|
||||
}
|
||||
|
||||
func initSpeedtestFlags() {
|
||||
speedtestCmd.Flags().BoolVar(&skipDownload, "skip-download", false, "Skip download test")
|
||||
speedtestCmd.Flags().BoolVar(&skipUpload, "skip-upload", false, "Skip upload test")
|
||||
speedtestCmd.Flags().Uint32Var(&dataSize, "data-size", 1024*1024*100, "Data size for download and upload tests")
|
||||
speedtestCmd.Flags().BoolVar(&useBytes, "use-bytes", false, "Use bytes per second instead of bits per second")
|
||||
}
|
||||
|
||||
func runSpeedtest(cmd *cobra.Command, args []string) {
|
||||
logger.Info("speed test mode")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
logger.Fatal("failed to read client config", zap.Error(err))
|
||||
}
|
||||
var config clientConfig
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
logger.Fatal("failed to parse client config", zap.Error(err))
|
||||
}
|
||||
hyConfig, err := config.Config()
|
||||
if err != nil {
|
||||
logger.Fatal("failed to load client config", zap.Error(err))
|
||||
}
|
||||
|
||||
c, info, err := client.NewClient(hyConfig)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to initialize client", zap.Error(err))
|
||||
}
|
||||
defer c.Close()
|
||||
logger.Info("connected to server",
|
||||
zap.Bool("udpEnabled", info.UDPEnabled),
|
||||
zap.Uint64("tx", info.Tx))
|
||||
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||
defer signal.Stop(signalChan)
|
||||
|
||||
runChan := make(chan struct{}, 1)
|
||||
go func() {
|
||||
if !skipDownload {
|
||||
runDownloadTest(c)
|
||||
}
|
||||
if !skipUpload {
|
||||
runUploadTest(c)
|
||||
}
|
||||
runChan <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-signalChan:
|
||||
logger.Info("received signal, shutting down gracefully")
|
||||
case <-runChan:
|
||||
logger.Info("speed test complete")
|
||||
}
|
||||
}
|
||||
|
||||
func runDownloadTest(c client.Client) {
|
||||
logger.Info("performing download test")
|
||||
downConn, err := c.TCP(speedtestAddr)
|
||||
if err != nil {
|
||||
if errors.As(err, &hyErrors.DialError{}) {
|
||||
logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err))
|
||||
} else {
|
||||
logger.Fatal("failed to connect", zap.Error(err))
|
||||
}
|
||||
}
|
||||
defer downConn.Close()
|
||||
|
||||
downClient := &speedtest.Client{Conn: downConn}
|
||||
currentTotal := uint32(0)
|
||||
err = downClient.Download(dataSize, func(d time.Duration, b uint32, done bool) {
|
||||
if !done {
|
||||
currentTotal += b
|
||||
logger.Info("downloading",
|
||||
zap.Uint32("bytes", b),
|
||||
zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)),
|
||||
zap.String("speed", formatSpeed(b, d, useBytes)))
|
||||
} else {
|
||||
logger.Info("download complete",
|
||||
zap.Uint32("bytes", b),
|
||||
zap.String("speed", formatSpeed(b, d, useBytes)))
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal("download test failed", zap.Error(err))
|
||||
}
|
||||
logger.Info("download test complete")
|
||||
}
|
||||
|
||||
func runUploadTest(c client.Client) {
|
||||
logger.Info("performing upload test")
|
||||
upConn, err := c.TCP(speedtestAddr)
|
||||
if err != nil {
|
||||
if errors.As(err, &hyErrors.DialError{}) {
|
||||
logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err))
|
||||
} else {
|
||||
logger.Fatal("failed to connect", zap.Error(err))
|
||||
}
|
||||
}
|
||||
defer upConn.Close()
|
||||
|
||||
upClient := &speedtest.Client{Conn: upConn}
|
||||
currentTotal := uint32(0)
|
||||
err = upClient.Upload(dataSize, func(d time.Duration, b uint32, done bool) {
|
||||
if !done {
|
||||
currentTotal += b
|
||||
logger.Info("uploading",
|
||||
zap.Uint32("bytes", b),
|
||||
zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)),
|
||||
zap.String("speed", formatSpeed(b, d, useBytes)))
|
||||
} else {
|
||||
logger.Info("upload complete",
|
||||
zap.Uint32("bytes", b),
|
||||
zap.String("speed", formatSpeed(b, d, useBytes)))
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal("upload test failed", zap.Error(err))
|
||||
}
|
||||
logger.Info("upload test complete")
|
||||
}
|
||||
|
||||
func formatSpeed(bytes uint32, duration time.Duration, useBytes bool) string {
|
||||
speed := float64(bytes) / duration.Seconds()
|
||||
var units []string
|
||||
if useBytes {
|
||||
units = []string{"B/s", "KB/s", "MB/s", "GB/s"}
|
||||
} else {
|
||||
units = []string{"bps", "Kbps", "Mbps", "Gbps"}
|
||||
speed *= 8
|
||||
}
|
||||
unitIndex := 0
|
||||
for speed > 1000 && unitIndex < len(units)-1 {
|
||||
speed /= 1000
|
||||
unitIndex++
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", speed, units[unitIndex])
|
||||
}
|
88
app/cmd/update.go
Normal file
88
app/cmd/update.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/apernet/hysteria/app/v2/internal/utils"
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
const (
|
||||
updateCheckInterval = 24 * time.Hour
|
||||
)
|
||||
|
||||
// checkUpdateCmd represents the checkUpdate command
|
||||
var checkUpdateCmd = &cobra.Command{
|
||||
Use: "check-update",
|
||||
Short: "Check for updates",
|
||||
Long: "Check for updates.",
|
||||
Run: runCheckUpdate,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(checkUpdateCmd)
|
||||
}
|
||||
|
||||
func runCheckUpdate(cmd *cobra.Command, args []string) {
|
||||
logger.Info("checking for updates",
|
||||
zap.String("version", appVersion),
|
||||
zap.String("platform", appPlatform),
|
||||
zap.String("arch", appArch),
|
||||
zap.String("channel", appType),
|
||||
)
|
||||
|
||||
checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType)
|
||||
resp, err := checker.Check()
|
||||
if err != nil {
|
||||
logger.Fatal("failed to check for updates", zap.Error(err))
|
||||
}
|
||||
if resp.HasUpdate {
|
||||
logger.Info("update available",
|
||||
zap.String("version", resp.LatestVersion),
|
||||
zap.String("url", resp.URL),
|
||||
zap.Bool("urgent", resp.Urgent),
|
||||
)
|
||||
} else {
|
||||
logger.Info("no update available")
|
||||
}
|
||||
}
|
||||
|
||||
// runCheckUpdateServer is the background update checking routine for server mode
|
||||
func runCheckUpdateServer() {
|
||||
checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType)
|
||||
checkUpdateRoutine(checker)
|
||||
}
|
||||
|
||||
// runCheckUpdateClient is the background update checking routine for client mode
|
||||
func runCheckUpdateClient(hyClient client.Client) {
|
||||
checker := utils.NewClientUpdateChecker(appVersion, appPlatform, appArch, appType, hyClient)
|
||||
checkUpdateRoutine(checker)
|
||||
}
|
||||
|
||||
func checkUpdateRoutine(checker *utils.UpdateChecker) {
|
||||
ticker := time.NewTicker(updateCheckInterval)
|
||||
for {
|
||||
logger.Debug("checking for updates",
|
||||
zap.String("version", appVersion),
|
||||
zap.String("platform", appPlatform),
|
||||
zap.String("arch", appArch),
|
||||
zap.String("channel", appType),
|
||||
)
|
||||
resp, err := checker.Check()
|
||||
if err != nil {
|
||||
logger.Debug("failed to check for updates", zap.Error(err))
|
||||
} else if resp.HasUpdate {
|
||||
logger.Info("update available",
|
||||
zap.String("version", resp.LatestVersion),
|
||||
zap.String("url", resp.URL),
|
||||
zap.Bool("urgent", resp.Urgent),
|
||||
)
|
||||
} else {
|
||||
logger.Debug("no update available")
|
||||
}
|
||||
<-ticker.C
|
||||
}
|
||||
}
|
23
app/cmd/version.go
Normal file
23
app/cmd/version.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show version",
|
||||
Long: "Show version.",
|
||||
Run: runVersion,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
func runVersion(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(appAboutLong)
|
||||
}
|
92
app/go.mod
Normal file
92
app/go.mod
Normal file
|
@ -0,0 +1,92 @@
|
|||
module github.com/apernet/hysteria/app/v2
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f
|
||||
github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000
|
||||
github.com/apernet/hysteria/extras/v2 v2.0.0-00010101000000-000000000000
|
||||
github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad
|
||||
github.com/caddyserver/certmagic v0.17.2
|
||||
github.com/libdns/cloudflare v0.1.1
|
||||
github.com/libdns/duckdns v0.2.0
|
||||
github.com/libdns/gandi v1.0.3
|
||||
github.com/libdns/godaddy v1.0.3
|
||||
github.com/libdns/namedotcom v0.3.3
|
||||
github.com/libdns/vultr v1.0.0
|
||||
github.com/mdp/qrterminal/v3 v3.1.1
|
||||
github.com/mholt/acmez v1.0.4
|
||||
github.com/sagernet/sing v0.3.2
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/sys v0.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431 // indirect
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect
|
||||
github.com/cloudflare/circl v1.3.9 // indirect
|
||||
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a // indirect
|
||||
github.com/database64128/tfo-go/v2 v2.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.6 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
|
||||
github.com/libdns/libdns v0.2.2 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/refraction-networking/utls v1.6.6 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||
github.com/vultr/govultr/v3 v3.6.4 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/oauth2 v0.20.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/apernet/hysteria/core/v2 => ../core
|
||||
|
||||
replace github.com/apernet/hysteria/extras/v2 => ../extras
|
659
app/go.sum
Normal file
659
app/go.sum
Normal file
|
@ -0,0 +1,659 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjgzx9vOcyCqsOY3c9kofDZ1n+qaw35ZY=
|
||||
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM=
|
||||
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431 h1:9/jM7e+kVALd7Jfu1c27dcEpT/Fd/Gzq2OsQjKjakKI=
|
||||
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431/go.mod h1:I/47OIGG5H/IfAm+nz2c6hm6b/NkEhpvptAoiPcY7jQ=
|
||||
github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad h1:QzQ2sKpc9o42HNRR8ukM5uMC/RzR2HgZd/Nvaqol2C0=
|
||||
github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad/go.mod h1:S5IydyLSN/QAfvY+r2GoomPJ6hidtXWm/Ad18sJVssk=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE=
|
||||
github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
|
||||
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a h1:t4SDi0pmNkryzKdM4QF3o5vqSP4GRjeZD/6j3nyxNP0=
|
||||
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a/go.mod h1:7K2NQKbabB5mBl41vF6YayYl5g7YpDwc4dQ5iMpP3Lg=
|
||||
github.com/database64128/tfo-go/v2 v2.2.2 h1:BxynF4qGF5ct3DpPLEG62uyJZ3LQhqaf0Ken+kyy7PM=
|
||||
github.com/database64128/tfo-go/v2 v2.2.2/go.mod h1:2IW8jppdBwdVMjA08uEyMNnqiAHKUlqAA+J8NrsfktY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0=
|
||||
github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
|
||||
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
|
||||
github.com/libdns/duckdns v0.2.0 h1:vd3pE09G2qTx1Zh1o3LmrivWSByD3Z5FbL7csX5vDgE=
|
||||
github.com/libdns/duckdns v0.2.0/go.mod h1:jCQ/7+qvhLK39+28qXvKEYGBBvmHBCmIwNqdJTCUmVs=
|
||||
github.com/libdns/gandi v1.0.3 h1:FIvipWOg/O4zi75fPRmtcolRKqI6MgrbpFy2p5KYdUk=
|
||||
github.com/libdns/gandi v1.0.3/go.mod h1:G6dw58Xnji2xX+lb+uZxGbtmfxKllm1CGHE2bOPG3WA=
|
||||
github.com/libdns/godaddy v1.0.3 h1:PX1FOYDQ1HGQzz8mVOmtwm3aa6Sv5MwCkNzivUUTA44=
|
||||
github.com/libdns/godaddy v1.0.3/go.mod h1:vuKWUXnvblDvcaiRwutOoLl7DuB21x8tI06owsF/JTM=
|
||||
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE=
|
||||
github.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s=
|
||||
github.com/libdns/vultr v1.0.0 h1:W8B4+k2bm9ro3bZLSZV9hMOQI+uO6Svu+GmD+Olz7ZI=
|
||||
github.com/libdns/vultr v1.0.0/go.mod h1:8K1HJExcbeHS4YPkFHRZpqpXZzZ+DZAA0m0VikJgEqk=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc=
|
||||
github.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU=
|
||||
github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
|
||||
github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
|
||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
|
||||
github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/sing v0.3.2 h1:CwWcxUBPkMvwgfe2/zUgY5oHG9qOL8Aob/evIFYK9jo=
|
||||
github.com/sagernet/sing v0.3.2/go.mod h1:qHySJ7u8po9DABtMYEkNBcOumx7ZZJf/fbv2sfTkNHE=
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
|
||||
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
|
||||
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM=
|
||||
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vultr/govultr/v3 v3.6.4 h1:unvY9eXlBw667ECQZDbBDOIaWB8wkk6Bx+yB0IMKXJ4=
|
||||
github.com/vultr/govultr/v3 v3.6.4/go.mod h1:rt9v2x114jZmmLAE/h5N5jnxTmsK9ewwS2oQZ0UBQzM=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
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-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/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.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
62
app/internal/forwarding/tcp.go
Normal file
62
app/internal/forwarding/tcp.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package forwarding
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
type TCPTunnel struct {
|
||||
HyClient client.Client
|
||||
Remote string
|
||||
EventLogger TCPEventLogger
|
||||
}
|
||||
|
||||
type TCPEventLogger interface {
|
||||
Connect(addr net.Addr)
|
||||
Error(addr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (t *TCPTunnel) Serve(listener net.Listener) error {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go t.handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TCPTunnel) handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.Connect(conn.RemoteAddr())
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.Error(conn.RemoteAddr(), closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
rc, err := t.HyClient.TCP(t.Remote)
|
||||
if err != nil {
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Start forwarding
|
||||
copyErrChan := make(chan error, 2)
|
||||
go func() {
|
||||
_, copyErr := io.Copy(rc, conn)
|
||||
copyErrChan <- copyErr
|
||||
}()
|
||||
go func() {
|
||||
_, copyErr := io.Copy(conn, rc)
|
||||
copyErrChan <- copyErr
|
||||
}()
|
||||
closeErr = <-copyErrChan
|
||||
}
|
39
app/internal/forwarding/tcp_test.go
Normal file
39
app/internal/forwarding/tcp_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package forwarding
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/apernet/hysteria/app/v2/internal/utils_test"
|
||||
)
|
||||
|
||||
func TestTCPTunnel(t *testing.T) {
|
||||
// Start the tunnel
|
||||
l, err := net.Listen("tcp", "127.0.0.1:34567")
|
||||
assert.NoError(t, err)
|
||||
defer l.Close()
|
||||
tunnel := &TCPTunnel{
|
||||
HyClient: &utils_test.MockEchoHyClient{},
|
||||
}
|
||||
go tunnel.Serve(l)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:34567")
|
||||
assert.NoError(t, err)
|
||||
|
||||
data := make([]byte, 1024)
|
||||
_, _ = rand.Read(data)
|
||||
_, err = conn.Write(data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
recv := make([]byte, 1024)
|
||||
_, err = conn.Read(recv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, data, recv)
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
180
app/internal/forwarding/udp.go
Normal file
180
app/internal/forwarding/udp.go
Normal file
|
@ -0,0 +1,180 @@
|
|||
package forwarding
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
const (
|
||||
udpBufferSize = 4096
|
||||
|
||||
defaultTimeout = 60 * time.Second
|
||||
idleCleanupInterval = 1 * time.Second
|
||||
)
|
||||
|
||||
type atomicTime struct {
|
||||
v atomic.Value
|
||||
}
|
||||
|
||||
func newAtomicTime(t time.Time) *atomicTime {
|
||||
a := &atomicTime{}
|
||||
a.Set(t)
|
||||
return a
|
||||
}
|
||||
|
||||
func (t *atomicTime) Set(new time.Time) {
|
||||
t.v.Store(new)
|
||||
}
|
||||
|
||||
func (t *atomicTime) Get() time.Time {
|
||||
return t.v.Load().(time.Time)
|
||||
}
|
||||
|
||||
type sessionEntry struct {
|
||||
HyConn client.HyUDPConn
|
||||
Last *atomicTime
|
||||
Timeout bool // true if the session is closed due to timeout
|
||||
}
|
||||
|
||||
func (e *sessionEntry) Feed(data []byte, addr string) error {
|
||||
e.Last.Set(time.Now())
|
||||
return e.HyConn.Send(data, addr)
|
||||
}
|
||||
|
||||
func (e *sessionEntry) ReceiveLoop(pc net.PacketConn, addr net.Addr) error {
|
||||
for {
|
||||
data, _, err := e.HyConn.Receive()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = pc.WriteTo(data, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.Last.Set(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
type UDPTunnel struct {
|
||||
HyClient client.Client
|
||||
Remote string
|
||||
Timeout time.Duration
|
||||
EventLogger UDPEventLogger
|
||||
|
||||
m map[string]*sessionEntry // addr -> HyConn
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
type UDPEventLogger interface {
|
||||
Connect(addr net.Addr)
|
||||
Error(addr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (t *UDPTunnel) Serve(pc net.PacketConn) error {
|
||||
t.m = make(map[string]*sessionEntry)
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
go t.idleCleanupLoop(stopCh)
|
||||
defer close(stopCh)
|
||||
defer t.cleanup(false)
|
||||
|
||||
buf := make([]byte, udpBufferSize)
|
||||
for {
|
||||
n, addr, err := pc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.feed(pc, addr, buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func (t *UDPTunnel) idleCleanupLoop(stopCh <-chan struct{}) {
|
||||
ticker := time.NewTicker(idleCleanupInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
t.cleanup(true)
|
||||
case <-stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *UDPTunnel) cleanup(idleOnly bool) {
|
||||
// We use RLock here as we are only scanning the map, not deleting from it.
|
||||
t.mutex.RLock()
|
||||
defer t.mutex.RUnlock()
|
||||
|
||||
timeout := t.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, entry := range t.m {
|
||||
if !idleOnly || now.Sub(entry.Last.Get()) > timeout {
|
||||
entry.Timeout = true
|
||||
_ = entry.HyConn.Close()
|
||||
// Closing the connection here will cause the ReceiveLoop to exit,
|
||||
// and the session will be removed from the map there.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *UDPTunnel) feed(pc net.PacketConn, addr net.Addr, data []byte) {
|
||||
t.mutex.RLock()
|
||||
entry := t.m[addr.String()]
|
||||
t.mutex.RUnlock()
|
||||
|
||||
// Create a new session if not exists
|
||||
if entry == nil {
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.Connect(addr)
|
||||
}
|
||||
hyConn, err := t.HyClient.UDP()
|
||||
if err != nil {
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.Error(addr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
entry = &sessionEntry{
|
||||
HyConn: hyConn,
|
||||
Last: newAtomicTime(time.Now()),
|
||||
}
|
||||
// Start the receive loop for this session
|
||||
// Local <- Remote
|
||||
go func() {
|
||||
err := entry.ReceiveLoop(pc, addr)
|
||||
if !entry.Timeout {
|
||||
_ = hyConn.Close()
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.Error(addr, err)
|
||||
}
|
||||
} else {
|
||||
// Connection already closed by timeout cleanup,
|
||||
// no need to close again here.
|
||||
// Use nil error to indicate timeout.
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.Error(addr, nil)
|
||||
}
|
||||
}
|
||||
// Remove the session from the map
|
||||
t.mutex.Lock()
|
||||
delete(t.m, addr.String())
|
||||
t.mutex.Unlock()
|
||||
}()
|
||||
// Insert the session into the map
|
||||
t.mutex.Lock()
|
||||
t.m[addr.String()] = entry
|
||||
t.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Feed the message to the session
|
||||
_ = entry.Feed(data, t.Remote)
|
||||
}
|
39
app/internal/forwarding/udp_test.go
Normal file
39
app/internal/forwarding/udp_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package forwarding
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/apernet/hysteria/app/v2/internal/utils_test"
|
||||
)
|
||||
|
||||
func TestUDPTunnel(t *testing.T) {
|
||||
// Start the tunnel
|
||||
l, err := net.ListenPacket("udp", "127.0.0.1:34567")
|
||||
assert.NoError(t, err)
|
||||
defer l.Close()
|
||||
tunnel := &UDPTunnel{
|
||||
HyClient: &utils_test.MockEchoHyClient{},
|
||||
}
|
||||
go tunnel.Serve(l)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
conn, err := net.Dial("udp", "127.0.0.1:34567")
|
||||
assert.NoError(t, err)
|
||||
|
||||
data := make([]byte, 1024)
|
||||
_, _ = rand.Read(data)
|
||||
_, err = conn.Write(data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
recv := make([]byte, 1024)
|
||||
_, err = conn.Read(recv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, data, recv)
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
301
app/internal/http/server.go
Normal file
301
app/internal/http/server.go
Normal file
|
@ -0,0 +1,301 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
const (
|
||||
httpClientTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// Server is an HTTP server using a Hysteria client as outbound.
|
||||
type Server struct {
|
||||
HyClient client.Client
|
||||
AuthFunc func(username, password string) bool // nil = no authentication
|
||||
AuthRealm string
|
||||
EventLogger EventLogger
|
||||
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type EventLogger interface {
|
||||
ConnectRequest(addr net.Addr, reqAddr string)
|
||||
ConnectError(addr net.Addr, reqAddr string, err error)
|
||||
HTTPRequest(addr net.Addr, reqURL string)
|
||||
HTTPError(addr net.Addr, reqURL string, err error)
|
||||
}
|
||||
|
||||
func (s *Server) Serve(listener net.Listener) error {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go s.dispatch(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) dispatch(conn net.Conn) {
|
||||
bufReader := bufio.NewReader(conn)
|
||||
for {
|
||||
req, err := http.ReadRequest(bufReader)
|
||||
if err != nil {
|
||||
// Connection error or invalid request
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
if s.AuthFunc != nil {
|
||||
authOK := false
|
||||
// Check the Proxy-Authorization header
|
||||
pAuth := req.Header.Get("Proxy-Authorization")
|
||||
if strings.HasPrefix(pAuth, "Basic ") {
|
||||
userPass, err := base64.URLEncoding.DecodeString(pAuth[6:])
|
||||
if err == nil {
|
||||
userPassParts := strings.SplitN(string(userPass), ":", 2)
|
||||
if len(userPassParts) == 2 {
|
||||
authOK = s.AuthFunc(userPassParts[0], userPassParts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !authOK {
|
||||
// Proxy authentication required
|
||||
_ = sendProxyAuthRequired(conn, req, s.AuthRealm)
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.Method == http.MethodConnect {
|
||||
if bufReader.Buffered() > 0 {
|
||||
// There is still data in the buffered reader.
|
||||
// We need to get it out and put it into a cachedConn,
|
||||
// so that handleConnect can read it.
|
||||
data := make([]byte, bufReader.Buffered())
|
||||
_, err := io.ReadFull(bufReader, data)
|
||||
if err != nil {
|
||||
// Read from buffer failed, is this possible?
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
cachedConn := &cachedConn{
|
||||
Conn: conn,
|
||||
Buffer: *bytes.NewBuffer(data),
|
||||
}
|
||||
s.handleConnect(cachedConn, req)
|
||||
} else {
|
||||
// No data in the buffered reader, we can just pass the original connection.
|
||||
s.handleConnect(conn, req)
|
||||
}
|
||||
// handleConnect will take over the connection,
|
||||
// i.e. it will not return until the connection is closed.
|
||||
// When it returns, there will be no more requests from this connection,
|
||||
// so we simply exit the loop.
|
||||
return
|
||||
} else {
|
||||
// handleRequest on the other hand handles one request at a time,
|
||||
// and returns when the request is done. It returns a bool indicating
|
||||
// whether the connection should be kept alive, but itself never closes
|
||||
// the connection.
|
||||
keepAlive := s.handleRequest(conn, req)
|
||||
if !keepAlive {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cachedConn is a net.Conn wrapper that first Read()s from a buffer,
|
||||
// and then from the underlying net.Conn when the buffer is drained.
|
||||
type cachedConn struct {
|
||||
net.Conn
|
||||
Buffer bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *cachedConn) Read(b []byte) (int, error) {
|
||||
if c.Buffer.Len() > 0 {
|
||||
n, err := c.Buffer.Read(b)
|
||||
if err == io.EOF {
|
||||
// Buffer is drained, hide it from the caller
|
||||
err = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
func (s *Server) handleConnect(conn net.Conn, req *http.Request) {
|
||||
defer conn.Close()
|
||||
|
||||
port := req.URL.Port()
|
||||
if port == "" {
|
||||
// HTTP defaults to port 80
|
||||
port = "80"
|
||||
}
|
||||
reqAddr := net.JoinHostPort(req.URL.Hostname(), port)
|
||||
|
||||
// Connect request & error log
|
||||
if s.EventLogger != nil {
|
||||
s.EventLogger.ConnectRequest(conn.RemoteAddr(), reqAddr)
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if s.EventLogger != nil {
|
||||
s.EventLogger.ConnectError(conn.RemoteAddr(), reqAddr, closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Dial
|
||||
rConn, err := s.HyClient.TCP(reqAddr)
|
||||
if err != nil {
|
||||
_ = sendSimpleResponse(conn, req, http.StatusBadGateway)
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
defer rConn.Close()
|
||||
|
||||
// Send 200 OK response and start relaying
|
||||
_ = sendSimpleResponse(conn, req, http.StatusOK)
|
||||
copyErrChan := make(chan error, 2)
|
||||
go func() {
|
||||
_, err := io.Copy(rConn, conn)
|
||||
copyErrChan <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(conn, rConn)
|
||||
copyErrChan <- err
|
||||
}()
|
||||
closeErr = <-copyErrChan
|
||||
}
|
||||
|
||||
func (s *Server) handleRequest(conn net.Conn, req *http.Request) bool {
|
||||
// Some clients use Connection, some use Proxy-Connection
|
||||
// https://www.oreilly.com/library/view/http-the-definitive/1565925092/re40.html
|
||||
keepAlive := req.ProtoAtLeast(1, 1) &&
|
||||
(strings.ToLower(req.Header.Get("Proxy-Connection")) == "keep-alive" ||
|
||||
strings.ToLower(req.Header.Get("Connection")) == "keep-alive")
|
||||
req.RequestURI = "" // Outgoing request should not have RequestURI
|
||||
|
||||
removeHopByHopHeaders(req.Header)
|
||||
removeExtraHTTPHostPort(req)
|
||||
|
||||
if req.URL.Scheme == "" || req.URL.Host == "" {
|
||||
_ = sendSimpleResponse(conn, req, http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
|
||||
// Request & error log
|
||||
if s.EventLogger != nil {
|
||||
s.EventLogger.HTTPRequest(conn.RemoteAddr(), req.URL.String())
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if s.EventLogger != nil {
|
||||
s.EventLogger.HTTPError(conn.RemoteAddr(), req.URL.String(), closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
if s.httpClient == nil {
|
||||
s.initHTTPClient()
|
||||
}
|
||||
|
||||
// Do the request and send the response back
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
closeErr = err
|
||||
_ = sendSimpleResponse(conn, req, http.StatusBadGateway)
|
||||
return false
|
||||
}
|
||||
|
||||
removeHopByHopHeaders(resp.Header)
|
||||
if keepAlive {
|
||||
resp.Header.Set("Connection", "keep-alive")
|
||||
resp.Header.Set("Proxy-Connection", "keep-alive")
|
||||
resp.Header.Set("Keep-Alive", "timeout=60")
|
||||
}
|
||||
|
||||
closeErr = resp.Write(conn)
|
||||
return closeErr == nil && keepAlive
|
||||
}
|
||||
|
||||
func (s *Server) initHTTPClient() {
|
||||
s.httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
// HyClient doesn't support context for now
|
||||
return s.HyClient.TCP(addr)
|
||||
},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: httpClientTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func removeHopByHopHeaders(header http.Header) {
|
||||
header.Del("Proxy-Connection") // Not in RFC but common
|
||||
// https://www.ietf.org/rfc/rfc2616.txt
|
||||
header.Del("Connection")
|
||||
header.Del("Keep-Alive")
|
||||
header.Del("Proxy-Authenticate")
|
||||
header.Del("Proxy-Authorization")
|
||||
header.Del("TE")
|
||||
header.Del("Trailers")
|
||||
header.Del("Transfer-Encoding")
|
||||
header.Del("Upgrade")
|
||||
}
|
||||
|
||||
func removeExtraHTTPHostPort(req *http.Request) {
|
||||
host := req.Host
|
||||
if host == "" {
|
||||
host = req.URL.Host
|
||||
}
|
||||
if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" {
|
||||
host = pHost
|
||||
}
|
||||
req.Host = host
|
||||
req.URL.Host = host
|
||||
}
|
||||
|
||||
// sendSimpleResponse sends a simple HTTP response with the given status code.
|
||||
func sendSimpleResponse(conn net.Conn, req *http.Request, statusCode int) error {
|
||||
resp := &http.Response{
|
||||
StatusCode: statusCode,
|
||||
Status: http.StatusText(statusCode),
|
||||
Proto: req.Proto,
|
||||
ProtoMajor: req.ProtoMajor,
|
||||
ProtoMinor: req.ProtoMinor,
|
||||
Header: http.Header{},
|
||||
}
|
||||
// Remove the "Content-Length: 0" header, some clients (e.g. ffmpeg) may not like it.
|
||||
resp.ContentLength = -1
|
||||
// Also, prevent the "Connection: close" header.
|
||||
resp.Close = false
|
||||
resp.Uncompressed = true
|
||||
return resp.Write(conn)
|
||||
}
|
||||
|
||||
// sendProxyAuthRequired sends a 407 Proxy Authentication Required response.
|
||||
func sendProxyAuthRequired(conn net.Conn, req *http.Request, realm string) error {
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusProxyAuthRequired,
|
||||
Status: http.StatusText(http.StatusProxyAuthRequired),
|
||||
Proto: req.Proto,
|
||||
ProtoMajor: req.ProtoMajor,
|
||||
ProtoMinor: req.ProtoMinor,
|
||||
Header: http.Header{},
|
||||
}
|
||||
resp.Header.Set("Proxy-Authenticate", fmt.Sprintf("Basic realm=%q", realm))
|
||||
return resp.Write(conn)
|
||||
}
|
59
app/internal/http/server_test.go
Normal file
59
app/internal/http/server_test.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
const (
|
||||
testCertFile = "test.crt"
|
||||
testKeyFile = "test.key"
|
||||
)
|
||||
|
||||
type mockHyClient struct{}
|
||||
|
||||
func (c *mockHyClient) TCP(addr string) (net.Conn, error) {
|
||||
return net.Dial("tcp", addr)
|
||||
}
|
||||
|
||||
func (c *mockHyClient) UDP() (client.HyUDPConn, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (c *mockHyClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
// Start the server
|
||||
l, err := net.Listen("tcp", "127.0.0.1:18080")
|
||||
assert.NoError(t, err)
|
||||
defer l.Close()
|
||||
s := &Server{
|
||||
HyClient: &mockHyClient{},
|
||||
}
|
||||
go s.Serve(l)
|
||||
|
||||
// Start a test HTTP & HTTPS server
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("control is an illusion"))
|
||||
})
|
||||
go http.ListenAndServe("127.0.0.1:18081", nil)
|
||||
go http.ListenAndServeTLS("127.0.0.1:18082", testCertFile, testKeyFile, nil)
|
||||
|
||||
// Run the Python test script
|
||||
cmd := exec.Command("python", "server_test.py")
|
||||
// Suppress HTTPS warning text from Python
|
||||
cmd.Env = append(cmd.Env, "PYTHONWARNINGS=ignore:Unverified HTTPS request")
|
||||
out, err := cmd.CombinedOutput()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "OK", strings.TrimSpace(string(out)))
|
||||
}
|
24
app/internal/http/server_test.py
Normal file
24
app/internal/http/server_test.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
import requests
|
||||
|
||||
proxies = {
|
||||
"http": "http://127.0.0.1:18080",
|
||||
"https": "http://127.0.0.1:18080",
|
||||
}
|
||||
|
||||
|
||||
def test_http(it):
|
||||
for i in range(it):
|
||||
r = requests.get("http://127.0.0.1:18081", proxies=proxies)
|
||||
assert r.status_code == 200 and r.text == "control is an illusion"
|
||||
|
||||
|
||||
def test_https(it):
|
||||
for i in range(it):
|
||||
r = requests.get("https://127.0.0.1:18082", proxies=proxies, verify=False)
|
||||
assert r.status_code == 200 and r.text == "control is an illusion"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_http(10)
|
||||
test_https(10)
|
||||
print("OK")
|
23
app/internal/http/test.crt
Normal file
23
app/internal/http/test.crt
Normal file
|
@ -0,0 +1,23 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL
|
||||
BQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM
|
||||
EFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3
|
||||
DQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy
|
||||
MDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE
|
||||
CgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI
|
||||
hvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||
AQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4
|
||||
ZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W
|
||||
DKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW
|
||||
CbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf
|
||||
jICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N
|
||||
o0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v
|
||||
M29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD
|
||||
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf
|
||||
1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc
|
||||
luDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4
|
||||
FjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD
|
||||
UMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD
|
||||
OLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7
|
||||
ydYKuI8=
|
||||
-----END CERTIFICATE-----
|
27
app/internal/http/test.key
Normal file
27
app/internal/http/test.key
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR
|
||||
2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr
|
||||
6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc
|
||||
L9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2
|
||||
wiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C
|
||||
LolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI
|
||||
MdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+
|
||||
FLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U
|
||||
64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX
|
||||
erwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu
|
||||
1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW
|
||||
T3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA
|
||||
g4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA
|
||||
o/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO
|
||||
Uj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY
|
||||
ZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V
|
||||
rFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k
|
||||
AnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI
|
||||
j8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0
|
||||
jS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2
|
||||
ZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g
|
||||
uhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+
|
||||
menJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2
|
||||
kh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB
|
||||
T7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ
|
||||
-----END RSA PRIVATE KEY-----
|
12
app/internal/proxymux/.mockery.yaml
Normal file
12
app/internal/proxymux/.mockery.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
with-expecter: true
|
||||
dir: internal/mocks
|
||||
outpkg: mocks
|
||||
packages:
|
||||
net:
|
||||
interfaces:
|
||||
Listener:
|
||||
config:
|
||||
mockname: MockListener
|
||||
Conn:
|
||||
config:
|
||||
mockname: MockConn
|
427
app/internal/proxymux/internal/mocks/mock_Conn.go
Normal file
427
app/internal/proxymux/internal/mocks/mock_Conn.go
Normal file
|
@ -0,0 +1,427 @@
|
|||
// Code generated by mockery v2.43.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
net "net"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
)
|
||||
|
||||
// MockConn is an autogenerated mock type for the Conn type
|
||||
type MockConn struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockConn_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockConn) EXPECT() *MockConn_Expecter {
|
||||
return &MockConn_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *MockConn) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Close")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type MockConn_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
func (_e *MockConn_Expecter) Close() *MockConn_Close_Call {
|
||||
return &MockConn_Close_Call{Call: _e.mock.On("Close")}
|
||||
}
|
||||
|
||||
func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// LocalAddr provides a mock function with given fields:
|
||||
func (_m *MockConn) LocalAddr() net.Addr {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for LocalAddr")
|
||||
}
|
||||
|
||||
var r0 net.Addr
|
||||
if rf, ok := ret.Get(0).(func() net.Addr); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(net.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr'
|
||||
type MockConn_LocalAddr_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// LocalAddr is a helper method to define mock.On call
|
||||
func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call {
|
||||
return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")}
|
||||
}
|
||||
|
||||
func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Read provides a mock function with given fields: b
|
||||
func (_m *MockConn) Read(b []byte) (int, error) {
|
||||
ret := _m.Called(b)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Read")
|
||||
}
|
||||
|
||||
var r0 int
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
|
||||
return rf(b)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func([]byte) int); ok {
|
||||
r0 = rf(b)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func([]byte) error); ok {
|
||||
r1 = rf(b)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
|
||||
type MockConn_Read_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Read is a helper method to define mock.On call
|
||||
// - b []byte
|
||||
func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call {
|
||||
return &MockConn_Read_Call{Call: _e.mock.On("Read", b)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].([]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call {
|
||||
_c.Call.Return(n, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoteAddr provides a mock function with given fields:
|
||||
func (_m *MockConn) RemoteAddr() net.Addr {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoteAddr")
|
||||
}
|
||||
|
||||
var r0 net.Addr
|
||||
if rf, ok := ret.Get(0).(func() net.Addr); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(net.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr'
|
||||
type MockConn_RemoteAddr_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoteAddr is a helper method to define mock.On call
|
||||
func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call {
|
||||
return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")}
|
||||
}
|
||||
|
||||
func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetDeadline provides a mock function with given fields: t
|
||||
func (_m *MockConn) SetDeadline(t time.Time) error {
|
||||
ret := _m.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetDeadline")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'
|
||||
type MockConn_SetDeadline_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetDeadline is a helper method to define mock.On call
|
||||
// - t time.Time
|
||||
func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call {
|
||||
return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(time.Time))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetReadDeadline provides a mock function with given fields: t
|
||||
func (_m *MockConn) SetReadDeadline(t time.Time) error {
|
||||
ret := _m.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetReadDeadline")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'
|
||||
type MockConn_SetReadDeadline_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetReadDeadline is a helper method to define mock.On call
|
||||
// - t time.Time
|
||||
func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call {
|
||||
return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(time.Time))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetWriteDeadline provides a mock function with given fields: t
|
||||
func (_m *MockConn) SetWriteDeadline(t time.Time) error {
|
||||
ret := _m.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetWriteDeadline")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'
|
||||
type MockConn_SetWriteDeadline_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetWriteDeadline is a helper method to define mock.On call
|
||||
// - t time.Time
|
||||
func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call {
|
||||
return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(time.Time))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Write provides a mock function with given fields: b
|
||||
func (_m *MockConn) Write(b []byte) (int, error) {
|
||||
ret := _m.Called(b)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Write")
|
||||
}
|
||||
|
||||
var r0 int
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
|
||||
return rf(b)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func([]byte) int); ok {
|
||||
r0 = rf(b)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func([]byte) error); ok {
|
||||
r1 = rf(b)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
|
||||
type MockConn_Write_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Write is a helper method to define mock.On call
|
||||
// - b []byte
|
||||
func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call {
|
||||
return &MockConn_Write_Call{Call: _e.mock.On("Write", b)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].([]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call {
|
||||
_c.Call.Return(n, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockConn(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockConn {
|
||||
mock := &MockConn{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
185
app/internal/proxymux/internal/mocks/mock_Listener.go
Normal file
185
app/internal/proxymux/internal/mocks/mock_Listener.go
Normal file
|
@ -0,0 +1,185 @@
|
|||
// Code generated by mockery v2.43.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
net "net"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockListener is an autogenerated mock type for the Listener type
|
||||
type MockListener struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockListener_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockListener) EXPECT() *MockListener_Expecter {
|
||||
return &MockListener_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Accept provides a mock function with given fields:
|
||||
func (_m *MockListener) Accept() (net.Conn, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Accept")
|
||||
}
|
||||
|
||||
var r0 net.Conn
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (net.Conn, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() net.Conn); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(net.Conn)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept'
|
||||
type MockListener_Accept_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Accept is a helper method to define mock.On call
|
||||
func (_e *MockListener_Expecter) Accept() *MockListener_Accept_Call {
|
||||
return &MockListener_Accept_Call{Call: _e.mock.On("Accept")}
|
||||
}
|
||||
|
||||
func (_c *MockListener_Accept_Call) Run(run func()) *MockListener_Accept_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockListener_Accept_Call) Return(_a0 net.Conn, _a1 error) *MockListener_Accept_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockListener_Accept_Call) RunAndReturn(run func() (net.Conn, error)) *MockListener_Accept_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Addr provides a mock function with given fields:
|
||||
func (_m *MockListener) Addr() net.Addr {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Addr")
|
||||
}
|
||||
|
||||
var r0 net.Addr
|
||||
if rf, ok := ret.Get(0).(func() net.Addr); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(net.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr'
|
||||
type MockListener_Addr_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Addr is a helper method to define mock.On call
|
||||
func (_e *MockListener_Expecter) Addr() *MockListener_Addr_Call {
|
||||
return &MockListener_Addr_Call{Call: _e.mock.On("Addr")}
|
||||
}
|
||||
|
||||
func (_c *MockListener_Addr_Call) Run(run func()) *MockListener_Addr_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockListener_Addr_Call) Return(_a0 net.Addr) *MockListener_Addr_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockListener_Addr_Call) RunAndReturn(run func() net.Addr) *MockListener_Addr_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *MockListener) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Close")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type MockListener_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
func (_e *MockListener_Expecter) Close() *MockListener_Close_Call {
|
||||
return &MockListener_Close_Call{Call: _e.mock.On("Close")}
|
||||
}
|
||||
|
||||
func (_c *MockListener_Close_Call) Run(run func()) *MockListener_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockListener_Close_Call) Return(_a0 error) *MockListener_Close_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockListener_Close_Call) RunAndReturn(run func() error) *MockListener_Close_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockListener creates a new instance of MockListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockListener(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockListener {
|
||||
mock := &MockListener{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
72
app/internal/proxymux/manager.go
Normal file
72
app/internal/proxymux/manager.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package proxymux
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/apernet/hysteria/extras/v2/correctnet"
|
||||
)
|
||||
|
||||
type muxManager struct {
|
||||
listeners map[string]*muxListener
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
var globalMuxManager *muxManager
|
||||
|
||||
func init() {
|
||||
globalMuxManager = &muxManager{
|
||||
listeners: make(map[string]*muxListener),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *muxManager) GetOrCreate(address string) (*muxListener, error) {
|
||||
key, err := m.canonicalizeAddrPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if ml, ok := m.listeners[key]; ok {
|
||||
return ml, nil
|
||||
}
|
||||
|
||||
listener, err := correctnet.Listen("tcp", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ml := newMuxListener(listener, func() {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
delete(m.listeners, key)
|
||||
})
|
||||
m.listeners[key] = ml
|
||||
return ml, nil
|
||||
}
|
||||
|
||||
func (m *muxManager) canonicalizeAddrPort(address string) (string, error) {
|
||||
taddr, err := net.ResolveTCPAddr("tcp", address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return taddr.String(), nil
|
||||
}
|
||||
|
||||
func ListenHTTP(address string) (net.Listener, error) {
|
||||
ml, err := globalMuxManager.GetOrCreate(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ml.ListenHTTP()
|
||||
}
|
||||
|
||||
func ListenSOCKS(address string) (net.Listener, error) {
|
||||
ml, err := globalMuxManager.GetOrCreate(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ml.ListenSOCKS()
|
||||
}
|
110
app/internal/proxymux/manager_test.go
Normal file
110
app/internal/proxymux/manager_test.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package proxymux
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestListenSOCKS(t *testing.T) {
|
||||
address := "127.2.39.129:11081"
|
||||
|
||||
sl, err := ListenSOCKS(address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
sl.Close()
|
||||
}()
|
||||
|
||||
hl, err := ListenHTTP(address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer hl.Close()
|
||||
|
||||
_, err = ListenSOCKS(address)
|
||||
if !assert.ErrorIs(t, err, ErrProtocolInUse) {
|
||||
return
|
||||
}
|
||||
sl.Close()
|
||||
|
||||
sl, err = ListenSOCKS(address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenHTTP(t *testing.T) {
|
||||
address := "127.2.39.129:11082"
|
||||
|
||||
hl, err := ListenHTTP(address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
hl.Close()
|
||||
}()
|
||||
|
||||
sl, err := ListenSOCKS(address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer sl.Close()
|
||||
|
||||
_, err = ListenHTTP(address)
|
||||
if !assert.ErrorIs(t, err, ErrProtocolInUse) {
|
||||
return
|
||||
}
|
||||
hl.Close()
|
||||
|
||||
hl, err = ListenHTTP(address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelease(t *testing.T) {
|
||||
address := "127.2.39.129:11083"
|
||||
|
||||
hl, err := ListenHTTP(address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
sl, err := ListenSOCKS(address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.True(t, globalMuxManager.testAddressExists(address)) {
|
||||
return
|
||||
}
|
||||
_, err = net.Listen("tcp", address)
|
||||
if !assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
hl.Close()
|
||||
sl.Close()
|
||||
|
||||
// Wait for muxListener released
|
||||
time.Sleep(time.Second)
|
||||
if !assert.False(t, globalMuxManager.testAddressExists(address)) {
|
||||
return
|
||||
}
|
||||
lis, err := net.Listen("tcp", address)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer lis.Close()
|
||||
}
|
||||
|
||||
func (m *muxManager) testAddressExists(address string) bool {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
_, ok := m.listeners[address]
|
||||
return ok
|
||||
}
|
320
app/internal/proxymux/mux.go
Normal file
320
app/internal/proxymux/mux.go
Normal file
|
@ -0,0 +1,320 @@
|
|||
package proxymux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func newMuxListener(listener net.Listener, deleteFunc func()) *muxListener {
|
||||
l := &muxListener{
|
||||
base: listener,
|
||||
acceptChan: make(chan net.Conn),
|
||||
closeChan: make(chan struct{}),
|
||||
deleteFunc: deleteFunc,
|
||||
}
|
||||
go l.acceptLoop()
|
||||
go l.mainLoop()
|
||||
return l
|
||||
}
|
||||
|
||||
type muxListener struct {
|
||||
lock sync.Mutex
|
||||
base net.Listener
|
||||
acceptErr error
|
||||
|
||||
acceptChan chan net.Conn
|
||||
closeChan chan struct{}
|
||||
|
||||
socksListener *subListener
|
||||
httpListener *subListener
|
||||
|
||||
deleteFunc func()
|
||||
}
|
||||
|
||||
func (l *muxListener) acceptLoop() {
|
||||
defer close(l.acceptChan)
|
||||
|
||||
for {
|
||||
conn, err := l.base.Accept()
|
||||
if err != nil {
|
||||
l.lock.Lock()
|
||||
l.acceptErr = err
|
||||
l.lock.Unlock()
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-l.closeChan:
|
||||
return
|
||||
case l.acceptChan <- conn:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *muxListener) mainLoop() {
|
||||
defer func() {
|
||||
l.deleteFunc()
|
||||
l.base.Close()
|
||||
|
||||
close(l.closeChan)
|
||||
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if sl := l.httpListener; sl != nil {
|
||||
close(sl.acceptChan)
|
||||
l.httpListener = nil
|
||||
}
|
||||
if sl := l.socksListener; sl != nil {
|
||||
close(sl.acceptChan)
|
||||
l.socksListener = nil
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
var socksCloseChan, httpCloseChan chan struct{}
|
||||
if l.httpListener != nil {
|
||||
httpCloseChan = l.httpListener.closeChan
|
||||
}
|
||||
if l.socksListener != nil {
|
||||
socksCloseChan = l.socksListener.closeChan
|
||||
}
|
||||
select {
|
||||
case <-l.closeChan:
|
||||
return
|
||||
case conn, ok := <-l.acceptChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
go l.dispatch(conn)
|
||||
case <-socksCloseChan:
|
||||
l.lock.Lock()
|
||||
if socksCloseChan == l.socksListener.closeChan {
|
||||
// not replaced by another ListenSOCKS()
|
||||
l.socksListener = nil
|
||||
}
|
||||
l.lock.Unlock()
|
||||
if l.checkIdle() {
|
||||
return
|
||||
}
|
||||
case <-httpCloseChan:
|
||||
l.lock.Lock()
|
||||
if httpCloseChan == l.httpListener.closeChan {
|
||||
// not replaced by another ListenHTTP()
|
||||
l.httpListener = nil
|
||||
}
|
||||
l.lock.Unlock()
|
||||
if l.checkIdle() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *muxListener) dispatch(conn net.Conn) {
|
||||
var b [1]byte
|
||||
if _, err := io.ReadFull(conn, b[:]); err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
l.lock.Lock()
|
||||
var target *subListener
|
||||
if b[0] == 5 {
|
||||
target = l.socksListener
|
||||
} else {
|
||||
target = l.httpListener
|
||||
}
|
||||
l.lock.Unlock()
|
||||
|
||||
if target == nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
wconn := &connWithOneByte{Conn: conn, b: b[0]}
|
||||
|
||||
select {
|
||||
case <-target.closeChan:
|
||||
case target.acceptChan <- wconn:
|
||||
}
|
||||
}
|
||||
|
||||
func (l *muxListener) checkIdle() bool {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
return l.httpListener == nil && l.socksListener == nil
|
||||
}
|
||||
|
||||
func (l *muxListener) getAndClearAcceptError() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.acceptErr == nil {
|
||||
return nil
|
||||
}
|
||||
err := l.acceptErr
|
||||
l.acceptErr = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *muxListener) ListenHTTP() (net.Listener, error) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.httpListener != nil {
|
||||
subListenerPendingClosed := false
|
||||
select {
|
||||
case <-l.httpListener.closeChan:
|
||||
subListenerPendingClosed = true
|
||||
default:
|
||||
}
|
||||
if !subListenerPendingClosed {
|
||||
return nil, OpErr{
|
||||
Addr: l.base.Addr(),
|
||||
Protocol: "http",
|
||||
Op: "bind-protocol",
|
||||
Err: ErrProtocolInUse,
|
||||
}
|
||||
}
|
||||
l.httpListener = nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-l.closeChan:
|
||||
return nil, net.ErrClosed
|
||||
default:
|
||||
}
|
||||
|
||||
sl := newSubListener(l.getAndClearAcceptError, l.base.Addr)
|
||||
l.httpListener = sl
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
func (l *muxListener) ListenSOCKS() (net.Listener, error) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.socksListener != nil {
|
||||
subListenerPendingClosed := false
|
||||
select {
|
||||
case <-l.socksListener.closeChan:
|
||||
subListenerPendingClosed = true
|
||||
default:
|
||||
}
|
||||
if !subListenerPendingClosed {
|
||||
return nil, OpErr{
|
||||
Addr: l.base.Addr(),
|
||||
Protocol: "socks",
|
||||
Op: "bind-protocol",
|
||||
Err: ErrProtocolInUse,
|
||||
}
|
||||
}
|
||||
l.socksListener = nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-l.closeChan:
|
||||
return nil, net.ErrClosed
|
||||
default:
|
||||
}
|
||||
|
||||
sl := newSubListener(l.getAndClearAcceptError, l.base.Addr)
|
||||
l.socksListener = sl
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
func newSubListener(acceptErrorFunc func() error, addrFunc func() net.Addr) *subListener {
|
||||
return &subListener{
|
||||
acceptChan: make(chan net.Conn),
|
||||
acceptErrorFunc: acceptErrorFunc,
|
||||
closeChan: make(chan struct{}),
|
||||
addrFunc: addrFunc,
|
||||
}
|
||||
}
|
||||
|
||||
type subListener struct {
|
||||
// receive connections or closure from upstream
|
||||
acceptChan chan net.Conn
|
||||
// get an error of Accept() from upstream
|
||||
acceptErrorFunc func() error
|
||||
// notify upstream that we are closed
|
||||
closeChan chan struct{}
|
||||
|
||||
// Listener.Addr() implementation of base listener
|
||||
addrFunc func() net.Addr
|
||||
}
|
||||
|
||||
func (l *subListener) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case <-l.closeChan:
|
||||
// closed by ourselves
|
||||
return nil, net.ErrClosed
|
||||
case conn, ok := <-l.acceptChan:
|
||||
if !ok {
|
||||
// closed by upstream
|
||||
if acceptErr := l.acceptErrorFunc(); acceptErr != nil {
|
||||
return nil, acceptErr
|
||||
}
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *subListener) Addr() net.Addr {
|
||||
return l.addrFunc()
|
||||
}
|
||||
|
||||
// Close implements net.Listener.Close.
|
||||
// Upstream should use close(l.acceptChan) instead.
|
||||
func (l *subListener) Close() error {
|
||||
select {
|
||||
case <-l.closeChan:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
close(l.closeChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// connWithOneByte is a net.Conn that returns b for the first read
|
||||
// request, then forwards everything else to Conn.
|
||||
type connWithOneByte struct {
|
||||
net.Conn
|
||||
|
||||
b byte
|
||||
bRead bool
|
||||
}
|
||||
|
||||
func (c *connWithOneByte) Read(bs []byte) (int, error) {
|
||||
if c.bRead {
|
||||
return c.Conn.Read(bs)
|
||||
}
|
||||
if len(bs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
c.bRead = true
|
||||
bs[0] = c.b
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
type OpErr struct {
|
||||
Addr net.Addr
|
||||
Protocol string
|
||||
Op string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (m OpErr) Error() string {
|
||||
return fmt.Sprintf("mux-listen: %s[%s]: %s: %v", m.Addr, m.Protocol, m.Op, m.Err)
|
||||
}
|
||||
|
||||
func (m OpErr) Unwrap() error {
|
||||
return m.Err
|
||||
}
|
||||
|
||||
var ErrProtocolInUse = errors.New("protocol already in use")
|
154
app/internal/proxymux/mux_test.go
Normal file
154
app/internal/proxymux/mux_test.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package proxymux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/app/v2/internal/proxymux/internal/mocks"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
//go:generate mockery
|
||||
|
||||
func testMockListener(t *testing.T, connChan <-chan net.Conn) net.Listener {
|
||||
closedChan := make(chan struct{})
|
||||
mockListener := mocks.NewMockListener(t)
|
||||
mockListener.EXPECT().Accept().RunAndReturn(func() (net.Conn, error) {
|
||||
select {
|
||||
case <-closedChan:
|
||||
return nil, net.ErrClosed
|
||||
case conn, ok := <-connChan:
|
||||
if !ok {
|
||||
panic("unexpected closed channel (connChan)")
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
})
|
||||
mockListener.EXPECT().Close().RunAndReturn(func() error {
|
||||
select {
|
||||
case <-closedChan:
|
||||
default:
|
||||
close(closedChan)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return mockListener
|
||||
}
|
||||
|
||||
func testMockConn(t *testing.T, b []byte) net.Conn {
|
||||
buf := bytes.NewReader(b)
|
||||
isClosed := false
|
||||
mockConn := mocks.NewMockConn(t)
|
||||
mockConn.EXPECT().Read(mock.Anything).RunAndReturn(func(b []byte) (int, error) {
|
||||
if isClosed {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
return buf.Read(b)
|
||||
})
|
||||
mockConn.EXPECT().Close().RunAndReturn(func() error {
|
||||
isClosed = true
|
||||
return nil
|
||||
})
|
||||
return mockConn
|
||||
}
|
||||
|
||||
func TestMuxHTTP(t *testing.T) {
|
||||
connChan := make(chan net.Conn)
|
||||
mockListener := testMockListener(t, connChan)
|
||||
mockConn := testMockConn(t, []byte("CONNECT example.com:443 HTTP/1.1\r\n\r\n"))
|
||||
|
||||
mux := newMuxListener(mockListener, func() {})
|
||||
hl, err := mux.ListenHTTP()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
sl, err := mux.ListenSOCKS()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
connChan <- mockConn
|
||||
|
||||
var socksConn, httpConn net.Conn
|
||||
var socksErr, httpErr error
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
socksConn, socksErr = sl.Accept()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
httpConn, httpErr = hl.Accept()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
sl.Close()
|
||||
hl.Close()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
assert.Nil(t, socksConn)
|
||||
assert.ErrorIs(t, socksErr, net.ErrClosed)
|
||||
assert.NotNil(t, httpConn)
|
||||
httpConn.Close()
|
||||
assert.NoError(t, httpErr)
|
||||
|
||||
// Wait for muxListener released
|
||||
<-mux.acceptChan
|
||||
}
|
||||
|
||||
func TestMuxSOCKS(t *testing.T) {
|
||||
connChan := make(chan net.Conn)
|
||||
mockListener := testMockListener(t, connChan)
|
||||
mockConn := testMockConn(t, []byte{0x05, 0x02, 0x00, 0x01}) // SOCKS5 Connect Request: NOAUTH+GSSAPI
|
||||
|
||||
mux := newMuxListener(mockListener, func() {})
|
||||
hl, err := mux.ListenHTTP()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
sl, err := mux.ListenSOCKS()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
connChan <- mockConn
|
||||
|
||||
var socksConn, httpConn net.Conn
|
||||
var socksErr, httpErr error
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
socksConn, socksErr = sl.Accept()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
httpConn, httpErr = hl.Accept()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
sl.Close()
|
||||
hl.Close()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
assert.NotNil(t, socksConn)
|
||||
socksConn.Close()
|
||||
assert.NoError(t, socksErr)
|
||||
assert.Nil(t, httpConn)
|
||||
assert.ErrorIs(t, httpErr, net.ErrClosed)
|
||||
|
||||
// Wait for muxListener released
|
||||
<-mux.acceptChan
|
||||
}
|
17
app/internal/redirect/getsockopt_linux.go
Normal file
17
app/internal/redirect/getsockopt_linux.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
//go:build !386
|
||||
// +build !386
|
||||
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) {
|
||||
_, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0)
|
||||
if e != 0 {
|
||||
err = e
|
||||
}
|
||||
return
|
||||
}
|
23
app/internal/redirect/getsockopt_linux_386.go
Normal file
23
app/internal/redirect/getsockopt_linux_386.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package redirect
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
sysGetsockopt = 15
|
||||
)
|
||||
|
||||
// On 386 we cannot call socketcall with syscall.Syscall6, as it always fails with EFAULT.
|
||||
// Use our own syscall.socketcall hack instead.
|
||||
|
||||
func syscall_socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno)
|
||||
|
||||
func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) {
|
||||
_, e := syscall_socketcall(sysGetsockopt, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0)
|
||||
if e != 0 {
|
||||
err = e
|
||||
}
|
||||
return
|
||||
}
|
7
app/internal/redirect/syscall_socketcall_linux_386.s
Normal file
7
app/internal/redirect/syscall_socketcall_linux_386.s
Normal file
|
@ -0,0 +1,7 @@
|
|||
//go:build gc
|
||||
// +build gc
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36
|
||||
JMP syscall·socketcall(SB)
|
126
app/internal/redirect/tcp_linux.go
Normal file
126
app/internal/redirect/tcp_linux.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package redirect
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
const (
|
||||
soOriginalDst = 80
|
||||
soOriginalDstV6 = 80
|
||||
)
|
||||
|
||||
type TCPRedirect struct {
|
||||
HyClient client.Client
|
||||
EventLogger TCPEventLogger
|
||||
}
|
||||
|
||||
type TCPEventLogger interface {
|
||||
Connect(addr, reqAddr net.Addr)
|
||||
Error(addr, reqAddr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error {
|
||||
listener, err := net.ListenTCP("tcp", laddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listener.Close()
|
||||
for {
|
||||
c, err := listener.AcceptTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go r.handle(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TCPRedirect) handle(conn *net.TCPConn) {
|
||||
defer conn.Close()
|
||||
dstAddr, err := getDstAddr(conn)
|
||||
if err != nil {
|
||||
// Fail silently if we can't get the original destination.
|
||||
// Maybe we should print something to the log?
|
||||
return
|
||||
}
|
||||
if r.EventLogger != nil {
|
||||
r.EventLogger.Connect(conn.RemoteAddr(), dstAddr)
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if r.EventLogger != nil {
|
||||
r.EventLogger.Error(conn.RemoteAddr(), dstAddr, closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
rc, err := r.HyClient.TCP(dstAddr.String())
|
||||
if err != nil {
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Start forwarding
|
||||
copyErrChan := make(chan error, 2)
|
||||
go func() {
|
||||
_, copyErr := io.Copy(rc, conn)
|
||||
copyErrChan <- copyErr
|
||||
}()
|
||||
go func() {
|
||||
_, copyErr := io.Copy(conn, rc)
|
||||
copyErrChan <- copyErr
|
||||
}()
|
||||
closeErr = <-copyErrChan
|
||||
}
|
||||
|
||||
type sockAddr struct {
|
||||
family uint16
|
||||
port [2]byte // always big endian regardless of platform
|
||||
data [24]byte // sockaddr_in or sockaddr_in6
|
||||
}
|
||||
|
||||
func getOriginalDst(fd uintptr) (*sockAddr, error) {
|
||||
var addr sockAddr
|
||||
addrSize := uint32(unsafe.Sizeof(addr))
|
||||
// Try IPv6 first
|
||||
err := getsockopt(fd, syscall.SOL_IPV6, soOriginalDstV6, unsafe.Pointer(&addr), &addrSize)
|
||||
if err == nil {
|
||||
return &addr, nil
|
||||
}
|
||||
// Then IPv4
|
||||
err = getsockopt(fd, syscall.SOL_IP, soOriginalDst, unsafe.Pointer(&addr), &addrSize)
|
||||
return &addr, err
|
||||
}
|
||||
|
||||
// getDstAddr returns the original destination of a redirected TCP connection.
|
||||
func getDstAddr(conn *net.TCPConn) (*net.TCPAddr, error) {
|
||||
rc, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var addr *sockAddr
|
||||
var err2 error
|
||||
err = rc.Control(func(fd uintptr) {
|
||||
addr, err2 = getOriginalDst(fd)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
switch addr.family {
|
||||
case syscall.AF_INET:
|
||||
return &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil
|
||||
case syscall.AF_INET6:
|
||||
return &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil
|
||||
default:
|
||||
return nil, errors.New("address family not IPv4 or IPv6")
|
||||
}
|
||||
}
|
24
app/internal/redirect/tcp_others.go
Normal file
24
app/internal/redirect/tcp_others.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
//go:build !linux
|
||||
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
type TCPRedirect struct {
|
||||
HyClient client.Client
|
||||
EventLogger TCPEventLogger
|
||||
}
|
||||
|
||||
type TCPEventLogger interface {
|
||||
Connect(addr, reqAddr net.Addr)
|
||||
Error(addr, reqAddr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error {
|
||||
return errors.New("not supported on this platform")
|
||||
}
|
65
app/internal/sockopts/fd_control_unix_socket_test.py
Normal file
65
app/internal/sockopts/fd_control_unix_socket_test.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import socket
|
||||
import array
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def serve(path):
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
if os.path.exists(path):
|
||||
raise
|
||||
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(path)
|
||||
server.listen()
|
||||
print(f"Listening on {path}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
connection, client_address = server.accept()
|
||||
print(f"Client connected")
|
||||
|
||||
try:
|
||||
# Receiving fd from client
|
||||
fds = array.array("i")
|
||||
msg, ancdata, flags, addr = connection.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i')))
|
||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
|
||||
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
||||
|
||||
fd = fds[0]
|
||||
|
||||
# We make a call to setsockopt(2) here, so client can verify we have received the fd
|
||||
# In the real scenario, the server would set things like SO_MARK,
|
||||
# we use SO_RCVBUF as it doesn't require any special capabilities.
|
||||
nbytes = struct.pack("i", 2500)
|
||||
fdsocket = fd_to_socket(fd)
|
||||
fdsocket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, nbytes)
|
||||
fdsocket.close()
|
||||
|
||||
# The only protocol-like thing specified in the client implementation.
|
||||
connection.send(b'\x01')
|
||||
finally:
|
||||
connection.close()
|
||||
print("Connection closed")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("Exit")
|
||||
|
||||
finally:
|
||||
server.close()
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def fd_to_socket(fd):
|
||||
return socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError("unix socket path is required")
|
||||
|
||||
serve(sys.argv[1])
|
76
app/internal/sockopts/sockopts.go
Normal file
76
app/internal/sockopts/sockopts.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package sockopts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type SocketOptions struct {
|
||||
BindInterface *string
|
||||
FirewallMark *uint32
|
||||
FdControlUnixSocket *string
|
||||
}
|
||||
|
||||
// implemented in platform-specific files
|
||||
var (
|
||||
bindInterfaceFunc func(c *net.UDPConn, device string) error
|
||||
firewallMarkFunc func(c *net.UDPConn, fwmark uint32) error
|
||||
fdControlUnixSocketFunc func(c *net.UDPConn, path string) error
|
||||
)
|
||||
|
||||
func (o *SocketOptions) CheckSupported() (err error) {
|
||||
if o.BindInterface != nil && bindInterfaceFunc == nil {
|
||||
return &UnsupportedError{"bindInterface"}
|
||||
}
|
||||
if o.FirewallMark != nil && firewallMarkFunc == nil {
|
||||
return &UnsupportedError{"fwmark"}
|
||||
}
|
||||
if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc == nil {
|
||||
return &UnsupportedError{"fdControlUnixSocket"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UnsupportedError struct {
|
||||
Field string
|
||||
}
|
||||
|
||||
func (e *UnsupportedError) Error() string {
|
||||
return fmt.Sprintf("%s is not supported on this platform", e.Field)
|
||||
}
|
||||
|
||||
func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) {
|
||||
uconn, err = net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = o.applyToUDPConn(uconn.(*net.UDPConn))
|
||||
if err != nil {
|
||||
uconn.Close()
|
||||
uconn = nil
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error {
|
||||
if o.BindInterface != nil && bindInterfaceFunc != nil {
|
||||
err := bindInterfaceFunc(c, *o.BindInterface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bind to interface: %w", err)
|
||||
}
|
||||
}
|
||||
if o.FirewallMark != nil && firewallMarkFunc != nil {
|
||||
err := firewallMarkFunc(c, *o.FirewallMark)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set fwmark: %w", err)
|
||||
}
|
||||
}
|
||||
if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil {
|
||||
err := fdControlUnixSocketFunc(c, *o.FdControlUnixSocket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send fd to control unix socket: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
96
app/internal/sockopts/sockopts_linux.go
Normal file
96
app/internal/sockopts/sockopts_linux.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
//go:build linux
|
||||
|
||||
package sockopts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
fdControlUnixTimeout = 3 * time.Second
|
||||
)
|
||||
|
||||
func init() {
|
||||
bindInterfaceFunc = bindInterfaceImpl
|
||||
firewallMarkFunc = firewallMarkImpl
|
||||
fdControlUnixSocketFunc = fdControlUnixSocketImpl
|
||||
}
|
||||
|
||||
func controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) {
|
||||
rconn, err := c.SyscallConn()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cerr := rconn.Control(func(fd uintptr) {
|
||||
err = cb(int(fd))
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if cerr != nil {
|
||||
err = fmt.Errorf("failed to control fd: %w", cerr)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func bindInterfaceImpl(c *net.UDPConn, device string) error {
|
||||
return controlUDPConn(c, func(fd int) error {
|
||||
return unix.BindToDevice(fd, device)
|
||||
})
|
||||
}
|
||||
|
||||
func firewallMarkImpl(c *net.UDPConn, fwmark uint32) error {
|
||||
return controlUDPConn(c, func(fd int) error {
|
||||
return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_MARK, int(fwmark))
|
||||
})
|
||||
}
|
||||
|
||||
func fdControlUnixSocketImpl(c *net.UDPConn, path string) error {
|
||||
return controlUDPConn(c, func(fd int) error {
|
||||
socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create unix socket: %w", err)
|
||||
}
|
||||
defer unix.Close(socketFd)
|
||||
|
||||
var timeout unix.Timeval
|
||||
timeUsec := fdControlUnixTimeout.Microseconds()
|
||||
castAssignInteger(timeUsec/1e6, &timeout.Sec)
|
||||
// Specifying the type explicitly is not necessary here, but it makes GoLand happy.
|
||||
castAssignInteger[int64](timeUsec%1e6, &timeout.Usec)
|
||||
|
||||
_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout)
|
||||
_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout)
|
||||
|
||||
err = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
|
||||
err = unix.Sendmsg(socketFd, nil, unix.UnixRights(fd), nil, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send: %w", err)
|
||||
}
|
||||
|
||||
dummy := []byte{1}
|
||||
n, err := unix.Read(socketFd, dummy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to receive: %w", err)
|
||||
}
|
||||
if n != 1 {
|
||||
return fmt.Errorf("socket closed unexpectedly")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func castAssignInteger[F, T constraints.Integer](from F, to *T) {
|
||||
*to = T(from)
|
||||
}
|
53
app/internal/sockopts/sockopts_linux_test.go
Normal file
53
app/internal/sockopts/sockopts_linux_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
//go:build linux
|
||||
|
||||
package sockopts
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func Test_fdControlUnixSocketImpl(t *testing.T) {
|
||||
sockPath := "./fd_control_unix_socket_test.sock"
|
||||
defer os.Remove(sockPath)
|
||||
|
||||
// Run test server
|
||||
cmd := exec.Command("python", "fd_control_unix_socket_test.py", sockPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Start()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
// Wait for the server to start
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
so := SocketOptions{
|
||||
FdControlUnixSocket: &sockPath,
|
||||
}
|
||||
conn, err := so.ListenUDP()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) {
|
||||
rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500),
|
||||
// and kernel will double this value for getsockopt().
|
||||
assert.Equal(t, 5000, rcvbuf)
|
||||
return
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
300
app/internal/socks5/server.go
Normal file
300
app/internal/socks5/server.go
Normal file
|
@ -0,0 +1,300 @@
|
|||
package socks5
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/txthinking/socks5"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
const udpBufferSize = 4096
|
||||
|
||||
// Server is a SOCKS5 server using a Hysteria client as outbound.
|
||||
type Server struct {
|
||||
HyClient client.Client
|
||||
AuthFunc func(username, password string) bool // nil = no authentication
|
||||
DisableUDP bool
|
||||
EventLogger EventLogger
|
||||
}
|
||||
|
||||
type EventLogger interface {
|
||||
TCPRequest(addr net.Addr, reqAddr string)
|
||||
TCPError(addr net.Addr, reqAddr string, err error)
|
||||
UDPRequest(addr net.Addr)
|
||||
UDPError(addr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (s *Server) Serve(listener net.Listener) error {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go s.dispatch(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) dispatch(conn net.Conn) {
|
||||
ok, _ := s.negotiate(conn)
|
||||
if !ok {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
// Negotiation ok, get and handle the request
|
||||
req, err := socks5.NewRequestFrom(conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
switch req.Cmd {
|
||||
case socks5.CmdConnect: // TCP
|
||||
s.handleTCP(conn, req)
|
||||
case socks5.CmdUDP: // UDP
|
||||
if s.DisableUDP {
|
||||
_ = sendSimpleReply(conn, socks5.RepCommandNotSupported)
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
s.handleUDP(conn, req)
|
||||
default:
|
||||
_ = sendSimpleReply(conn, socks5.RepCommandNotSupported)
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) negotiate(conn net.Conn) (bool, error) {
|
||||
req, err := socks5.NewNegotiationRequestFrom(conn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var serverMethod byte
|
||||
if s.AuthFunc != nil {
|
||||
serverMethod = socks5.MethodUsernamePassword
|
||||
} else {
|
||||
serverMethod = socks5.MethodNone
|
||||
}
|
||||
// Look for the supported method in the client request
|
||||
supported := false
|
||||
for _, m := range req.Methods {
|
||||
if m == serverMethod {
|
||||
supported = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !supported {
|
||||
// No supported method found, reject the client
|
||||
rep := socks5.NewNegotiationReply(socks5.MethodUnsupportAll)
|
||||
_, err := rep.WriteTo(conn)
|
||||
return false, err
|
||||
}
|
||||
// OK, send the method we chose
|
||||
rep := socks5.NewNegotiationReply(serverMethod)
|
||||
_, err = rep.WriteTo(conn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// If we chose the username/password method, authenticate the client
|
||||
if serverMethod == socks5.MethodUsernamePassword {
|
||||
req, err := socks5.NewUserPassNegotiationRequestFrom(conn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
ok := s.AuthFunc(string(req.Uname), string(req.Passwd))
|
||||
if ok {
|
||||
rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess)
|
||||
_, err := rep.WriteTo(conn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure)
|
||||
_, err := rep.WriteTo(conn)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleTCP(conn net.Conn, req *socks5.Request) {
|
||||
defer conn.Close()
|
||||
|
||||
addr := req.Address()
|
||||
|
||||
// TCP request & error log
|
||||
if s.EventLogger != nil {
|
||||
s.EventLogger.TCPRequest(conn.RemoteAddr(), addr)
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if s.EventLogger != nil {
|
||||
s.EventLogger.TCPError(conn.RemoteAddr(), addr, closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Dial
|
||||
rConn, err := s.HyClient.TCP(addr)
|
||||
if err != nil {
|
||||
_ = sendSimpleReply(conn, socks5.RepHostUnreachable)
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
defer rConn.Close()
|
||||
|
||||
// Send reply and start relaying
|
||||
_ = sendSimpleReply(conn, socks5.RepSuccess)
|
||||
copyErrChan := make(chan error, 2)
|
||||
go func() {
|
||||
_, err := io.Copy(rConn, conn)
|
||||
copyErrChan <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(conn, rConn)
|
||||
copyErrChan <- err
|
||||
}()
|
||||
closeErr = <-copyErrChan
|
||||
}
|
||||
|
||||
func (s *Server) handleUDP(conn net.Conn, req *socks5.Request) {
|
||||
defer conn.Close()
|
||||
|
||||
// UDP request & error log
|
||||
if s.EventLogger != nil {
|
||||
s.EventLogger.UDPRequest(conn.RemoteAddr())
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if s.EventLogger != nil {
|
||||
s.EventLogger.UDPError(conn.RemoteAddr(), closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start UDP relay server
|
||||
// SOCKS5 UDP requires the server to return the UDP bind address and port in the reply.
|
||||
// We bind to the same address that our TCP server listens on (but a different port).
|
||||
host, _, err := net.SplitHostPort(conn.LocalAddr().String())
|
||||
if err != nil {
|
||||
// Is this even possible?
|
||||
_ = sendSimpleReply(conn, socks5.RepServerFailure)
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, "0"))
|
||||
if err != nil {
|
||||
_ = sendSimpleReply(conn, socks5.RepServerFailure)
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
_ = sendSimpleReply(conn, socks5.RepServerFailure)
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
// HyClient UDP session
|
||||
hyUDP, err := s.HyClient.UDP()
|
||||
if err != nil {
|
||||
_ = sendSimpleReply(conn, socks5.RepServerFailure)
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
defer hyUDP.Close()
|
||||
|
||||
// Send reply
|
||||
_ = sendUDPReply(conn, udpConn.LocalAddr().(*net.UDPAddr))
|
||||
|
||||
// UDP relay & SOCKS5 connection holder
|
||||
errChan := make(chan error, 2)
|
||||
go func() {
|
||||
err := s.udpServer(udpConn, hyUDP)
|
||||
errChan <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(io.Discard, conn)
|
||||
errChan <- err
|
||||
}()
|
||||
closeErr = <-errChan
|
||||
}
|
||||
|
||||
func (s *Server) udpServer(udpConn *net.UDPConn, hyUDP client.HyUDPConn) error {
|
||||
var clientAddr *net.UDPAddr
|
||||
buf := make([]byte, udpBufferSize)
|
||||
// local -> remote
|
||||
for {
|
||||
n, cAddr, err := udpConn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := socks5.NewDatagramFromBytes(buf[:n])
|
||||
if err != nil || d.Frag != 0 {
|
||||
// Ignore bad packets
|
||||
// Also we don't support SOCKS5 UDP fragmentation for now
|
||||
continue
|
||||
}
|
||||
if clientAddr == nil {
|
||||
// Before the first packet, we don't know what IP the client will use to send us packets,
|
||||
// so we don't know what IP to return packets to.
|
||||
// We treat whoever sends us the first packet as our client.
|
||||
clientAddr = cAddr
|
||||
// Now that we know the client's address, we can start the
|
||||
// remote -> local direction.
|
||||
go func() {
|
||||
for {
|
||||
bs, from, err := hyUDP.Receive()
|
||||
if err != nil {
|
||||
// Close the UDP conn so that the local -> remote direction will exit
|
||||
_ = udpConn.Close()
|
||||
return
|
||||
}
|
||||
atyp, addr, port, err := socks5.ParseAddress(from)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if atyp == socks5.ATYPDomain {
|
||||
// socks5.ParseAddress adds a leading byte for domains,
|
||||
// but socks5.NewDatagram will add it again as it expects a raw domain.
|
||||
// So we must remove it here.
|
||||
addr = addr[1:]
|
||||
}
|
||||
d := socks5.NewDatagram(atyp, addr, port, bs)
|
||||
_, _ = udpConn.WriteToUDP(d.Bytes(), clientAddr)
|
||||
}
|
||||
}()
|
||||
} else if !clientAddr.IP.Equal(cAddr.IP) || clientAddr.Port != cAddr.Port {
|
||||
// Not our client, ignore
|
||||
continue
|
||||
}
|
||||
// Send to remote
|
||||
_ = hyUDP.Send(d.Data, d.Address())
|
||||
}
|
||||
}
|
||||
|
||||
// sendSimpleReply sends a SOCKS5 reply with the given reply code.
|
||||
// It does not contain bind address or port, so it's not suitable for successful UDP requests.
|
||||
func sendSimpleReply(conn net.Conn, rep byte) error {
|
||||
p := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00})
|
||||
_, err := p.WriteTo(conn)
|
||||
return err
|
||||
}
|
||||
|
||||
// sendUDPReply sends a SOCKS5 reply with the given reply code and bind address/port.
|
||||
func sendUDPReply(conn net.Conn, addr *net.UDPAddr) error {
|
||||
var atyp byte
|
||||
var bndAddr, bndPort []byte
|
||||
if ip4 := addr.IP.To4(); ip4 != nil {
|
||||
atyp = socks5.ATYPIPv4
|
||||
bndAddr = ip4
|
||||
} else {
|
||||
atyp = socks5.ATYPIPv6
|
||||
bndAddr = addr.IP
|
||||
}
|
||||
bndPort = make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(bndPort, uint16(addr.Port))
|
||||
p := socks5.NewReply(socks5.RepSuccess, atyp, bndAddr, bndPort)
|
||||
_, err := p.WriteTo(conn)
|
||||
return err
|
||||
}
|
29
app/internal/socks5/server_test.go
Normal file
29
app/internal/socks5/server_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package socks5
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/apernet/hysteria/app/v2/internal/utils_test"
|
||||
)
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
// Start the server
|
||||
l, err := net.Listen("tcp", "127.0.0.1:11080")
|
||||
assert.NoError(t, err)
|
||||
defer l.Close()
|
||||
s := &Server{
|
||||
HyClient: &utils_test.MockEchoHyClient{},
|
||||
}
|
||||
go s.Serve(l)
|
||||
|
||||
// Run the Python test script
|
||||
cmd := exec.Command("python", "server_test.py")
|
||||
out, err := cmd.CombinedOutput()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "OK", strings.TrimSpace(string(out)))
|
||||
}
|
57
app/internal/socks5/server_test.py
Normal file
57
app/internal/socks5/server_test.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
import socket
|
||||
import socks
|
||||
import os
|
||||
|
||||
ADDR = "127.0.0.1"
|
||||
PORT = 11080
|
||||
|
||||
|
||||
def test_tcp(size, count, it, domain=False):
|
||||
for i in range(it):
|
||||
s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.set_proxy(socks.SOCKS5, ADDR, PORT)
|
||||
|
||||
if domain:
|
||||
s.connect(("test.tcp.com", 12345))
|
||||
else:
|
||||
s.connect(("1.2.3.4", 12345))
|
||||
|
||||
for j in range(count):
|
||||
payload = os.urandom(size)
|
||||
s.send(payload)
|
||||
rsp = s.recv(size)
|
||||
assert rsp == payload
|
||||
|
||||
s.close()
|
||||
|
||||
|
||||
def test_udp(size, count, it, domain=False):
|
||||
for i in range(it):
|
||||
s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.set_proxy(socks.SOCKS5, ADDR, PORT)
|
||||
|
||||
for j in range(count):
|
||||
payload = os.urandom(size)
|
||||
|
||||
if domain:
|
||||
s.sendto(payload, ("test.udp.com", 12345))
|
||||
else:
|
||||
s.sendto(payload, ("1.2.3.4", 12345))
|
||||
|
||||
rsp, addr = s.recvfrom(size)
|
||||
assert rsp == payload
|
||||
|
||||
if domain:
|
||||
assert addr == (b"test.udp.com", 12345)
|
||||
else:
|
||||
assert addr == ("1.2.3.4", 12345)
|
||||
|
||||
s.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_tcp(1024, 1024, 10, domain=False)
|
||||
test_tcp(1024, 1024, 10, domain=True)
|
||||
test_udp(1024, 1024, 10, domain=False)
|
||||
test_udp(1024, 1024, 10, domain=True)
|
||||
print("OK")
|
69
app/internal/tproxy/tcp_linux.go
Normal file
69
app/internal/tproxy/tcp_linux.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package tproxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/apernet/go-tproxy"
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
type TCPTProxy struct {
|
||||
HyClient client.Client
|
||||
EventLogger TCPEventLogger
|
||||
}
|
||||
|
||||
type TCPEventLogger interface {
|
||||
Connect(addr, reqAddr net.Addr)
|
||||
Error(addr, reqAddr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error {
|
||||
listener, err := tproxy.ListenTCP("tcp", laddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listener.Close()
|
||||
for {
|
||||
c, err := listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go r.handle(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TCPTProxy) handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
// In TProxy mode, we are masquerading as the remote server.
|
||||
// So LocalAddr is actually the target the user is trying to connect to,
|
||||
// and RemoteAddr is the local address.
|
||||
if r.EventLogger != nil {
|
||||
r.EventLogger.Connect(conn.RemoteAddr(), conn.LocalAddr())
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if r.EventLogger != nil {
|
||||
r.EventLogger.Error(conn.RemoteAddr(), conn.LocalAddr(), closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
rc, err := r.HyClient.TCP(conn.LocalAddr().String())
|
||||
if err != nil {
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Start forwarding
|
||||
copyErrChan := make(chan error, 2)
|
||||
go func() {
|
||||
_, copyErr := io.Copy(rc, conn)
|
||||
copyErrChan <- copyErr
|
||||
}()
|
||||
go func() {
|
||||
_, copyErr := io.Copy(conn, rc)
|
||||
copyErrChan <- copyErr
|
||||
}()
|
||||
closeErr = <-copyErrChan
|
||||
}
|
24
app/internal/tproxy/tcp_others.go
Normal file
24
app/internal/tproxy/tcp_others.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
//go:build !linux
|
||||
|
||||
package tproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
type TCPTProxy struct {
|
||||
HyClient client.Client
|
||||
EventLogger TCPEventLogger
|
||||
}
|
||||
|
||||
type TCPEventLogger interface {
|
||||
Connect(addr, reqAddr net.Addr)
|
||||
Error(addr, reqAddr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error {
|
||||
return errors.New("not supported on this platform")
|
||||
}
|
140
app/internal/tproxy/udp_linux.go
Normal file
140
app/internal/tproxy/udp_linux.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package tproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/go-tproxy"
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
const (
|
||||
udpBufferSize = 4096
|
||||
defaultTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
type UDPTProxy struct {
|
||||
HyClient client.Client
|
||||
Timeout time.Duration
|
||||
EventLogger UDPEventLogger
|
||||
}
|
||||
|
||||
type UDPEventLogger interface {
|
||||
Connect(addr, reqAddr net.Addr)
|
||||
Error(addr, reqAddr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error {
|
||||
conn, err := tproxy.ListenUDP("udp", laddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
buf := make([]byte, udpBufferSize)
|
||||
for {
|
||||
// We will only get the first packet of each src/dst pair here,
|
||||
// because newPair will create a TProxy connection and take over
|
||||
// the src/dst pair. Later packets will be sent there instead of here.
|
||||
n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.newPair(srcAddr, dstAddr, buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UDPTProxy) newPair(srcAddr, dstAddr *net.UDPAddr, initPkt []byte) {
|
||||
if r.EventLogger != nil {
|
||||
r.EventLogger.Connect(srcAddr, dstAddr)
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
// If closeErr is nil, it means we at least successfully sent the first packet
|
||||
// and started forwarding, in which case we don't call the error logger.
|
||||
if r.EventLogger != nil && closeErr != nil {
|
||||
r.EventLogger.Error(srcAddr, dstAddr, closeErr)
|
||||
}
|
||||
}()
|
||||
conn, err := tproxy.DialUDP("udp", dstAddr, srcAddr)
|
||||
if err != nil {
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
hyConn, err := r.HyClient.UDP()
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
// Send the first packet
|
||||
err = hyConn.Send(initPkt, dstAddr.String())
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
_ = hyConn.Close()
|
||||
closeErr = err
|
||||
return
|
||||
}
|
||||
// Start forwarding
|
||||
go func() {
|
||||
err := r.forwarding(conn, hyConn, dstAddr.String())
|
||||
_ = conn.Close()
|
||||
_ = hyConn.Close()
|
||||
if r.EventLogger != nil {
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
// We don't consider deadline exceeded (timeout) an error
|
||||
err = nil
|
||||
}
|
||||
r.EventLogger.Error(srcAddr, dstAddr, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *UDPTProxy) forwarding(conn *net.UDPConn, hyConn client.HyUDPConn, dst string) error {
|
||||
errChan := make(chan error, 2)
|
||||
// Local <- Remote
|
||||
go func() {
|
||||
for {
|
||||
bs, _, err := hyConn.Receive()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
_, err = conn.Write(bs)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
_ = r.updateConnDeadline(conn)
|
||||
}
|
||||
}()
|
||||
// Local -> Remote
|
||||
go func() {
|
||||
buf := make([]byte, udpBufferSize)
|
||||
for {
|
||||
_ = r.updateConnDeadline(conn)
|
||||
n, err := conn.Read(buf)
|
||||
if n > 0 {
|
||||
err := hyConn.Send(buf[:n], dst)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return <-errChan
|
||||
}
|
||||
|
||||
func (r *UDPTProxy) updateConnDeadline(conn *net.UDPConn) error {
|
||||
if r.Timeout == 0 {
|
||||
return conn.SetReadDeadline(time.Now().Add(defaultTimeout))
|
||||
} else {
|
||||
return conn.SetReadDeadline(time.Now().Add(r.Timeout))
|
||||
}
|
||||
}
|
26
app/internal/tproxy/udp_others.go
Normal file
26
app/internal/tproxy/udp_others.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
//go:build !linux
|
||||
|
||||
package tproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
type UDPTProxy struct {
|
||||
HyClient client.Client
|
||||
Timeout time.Duration
|
||||
EventLogger UDPEventLogger
|
||||
}
|
||||
|
||||
type UDPEventLogger interface {
|
||||
Connect(addr, reqAddr net.Addr)
|
||||
Error(addr, reqAddr net.Addr, err error)
|
||||
}
|
||||
|
||||
func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error {
|
||||
return errors.New("not supported on this platform")
|
||||
}
|
14
app/internal/tun/check_ipv6_others.go
Normal file
14
app/internal/tun/check_ipv6_others.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
//go:build !unix && !windows
|
||||
|
||||
package tun
|
||||
|
||||
import "net"
|
||||
|
||||
func isIPv6Supported() bool {
|
||||
lis, err := net.ListenPacket("udp6", "[::1]:0")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = lis.Close()
|
||||
return true
|
||||
}
|
16
app/internal/tun/check_ipv6_unix.go
Normal file
16
app/internal/tun/check_ipv6_unix.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
//go:build unix
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func isIPv6Supported() bool {
|
||||
sock, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = unix.Close(sock)
|
||||
return true
|
||||
}
|
24
app/internal/tun/check_ipv6_windows.go
Normal file
24
app/internal/tun/check_ipv6_windows.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
//go:build windows
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func isIPv6Supported() bool {
|
||||
var wsaData windows.WSAData
|
||||
err := windows.WSAStartup(uint32(0x202), &wsaData)
|
||||
if err != nil {
|
||||
// Failing silently: it is not our duty to report such errors
|
||||
return true
|
||||
}
|
||||
defer windows.WSACleanup()
|
||||
|
||||
sock, err := windows.Socket(windows.AF_INET6, windows.SOCK_DGRAM, windows.IPPROTO_UDP)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = windows.Closesocket(sock)
|
||||
return true
|
||||
}
|
77
app/internal/tun/log.go
Normal file
77
app/internal/tun/log.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package tun
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var _ logger.Logger = (*singLogger)(nil)
|
||||
|
||||
type singLogger struct {
|
||||
tag string
|
||||
zapLogger *zap.Logger
|
||||
}
|
||||
|
||||
func extractSingExceptions(args []any) {
|
||||
for i, arg := range args {
|
||||
if err, ok := arg.(error); ok {
|
||||
args[i] = err.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *singLogger) Trace(args ...any) {
|
||||
if l.zapLogger == nil {
|
||||
return
|
||||
}
|
||||
extractSingExceptions(args)
|
||||
l.zapLogger.Debug(l.tag, zap.Any("args", args))
|
||||
}
|
||||
|
||||
func (l *singLogger) Debug(args ...any) {
|
||||
if l.zapLogger == nil {
|
||||
return
|
||||
}
|
||||
extractSingExceptions(args)
|
||||
l.zapLogger.Debug(l.tag, zap.Any("args", args))
|
||||
}
|
||||
|
||||
func (l *singLogger) Info(args ...any) {
|
||||
if l.zapLogger == nil {
|
||||
return
|
||||
}
|
||||
extractSingExceptions(args)
|
||||
l.zapLogger.Info(l.tag, zap.Any("args", args))
|
||||
}
|
||||
|
||||
func (l *singLogger) Warn(args ...any) {
|
||||
if l.zapLogger == nil {
|
||||
return
|
||||
}
|
||||
extractSingExceptions(args)
|
||||
l.zapLogger.Warn(l.tag, zap.Any("args", args))
|
||||
}
|
||||
|
||||
func (l *singLogger) Error(args ...any) {
|
||||
if l.zapLogger == nil {
|
||||
return
|
||||
}
|
||||
extractSingExceptions(args)
|
||||
l.zapLogger.Error(l.tag, zap.Any("args", args))
|
||||
}
|
||||
|
||||
func (l *singLogger) Fatal(args ...any) {
|
||||
if l.zapLogger == nil {
|
||||
return
|
||||
}
|
||||
extractSingExceptions(args)
|
||||
l.zapLogger.Fatal(l.tag, zap.Any("args", args))
|
||||
}
|
||||
|
||||
func (l *singLogger) Panic(args ...any) {
|
||||
if l.zapLogger == nil {
|
||||
return
|
||||
}
|
||||
extractSingExceptions(args)
|
||||
l.zapLogger.Panic(l.tag, zap.Any("args", args))
|
||||
}
|
234
app/internal/tun/server.go
Normal file
234
app/internal/tun/server.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
tun "github.com/apernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
"github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/network"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
HyClient client.Client
|
||||
EventLogger EventLogger
|
||||
|
||||
// for debugging
|
||||
Logger *zap.Logger
|
||||
|
||||
IfName string
|
||||
MTU uint32
|
||||
Timeout int64 // in seconds, also applied to TCP in system stack
|
||||
|
||||
// required by system stack
|
||||
Inet4Address []netip.Prefix
|
||||
Inet6Address []netip.Prefix
|
||||
|
||||
// auto route
|
||||
AutoRoute bool
|
||||
StructRoute bool
|
||||
Inet4RouteAddress []netip.Prefix
|
||||
Inet6RouteAddress []netip.Prefix
|
||||
Inet4RouteExcludeAddress []netip.Prefix
|
||||
Inet6RouteExcludeAddress []netip.Prefix
|
||||
}
|
||||
|
||||
type EventLogger interface {
|
||||
TCPRequest(addr, reqAddr string)
|
||||
TCPError(addr, reqAddr string, err error)
|
||||
UDPRequest(addr string)
|
||||
UDPError(addr string, err error)
|
||||
}
|
||||
|
||||
func (s *Server) Serve() error {
|
||||
if !isIPv6Supported() {
|
||||
s.Logger.Warn("tun-pre-check", zap.String("msg", "IPv6 is not supported or enabled on this system, TUN device is created without IPv6 support."))
|
||||
s.Inet6Address = nil
|
||||
}
|
||||
tunOpts := tun.Options{
|
||||
Name: s.IfName,
|
||||
Inet4Address: s.Inet4Address,
|
||||
Inet6Address: s.Inet6Address,
|
||||
MTU: s.MTU,
|
||||
GSO: true,
|
||||
AutoRoute: s.AutoRoute,
|
||||
StrictRoute: s.StructRoute,
|
||||
Inet4RouteAddress: s.Inet4RouteAddress,
|
||||
Inet6RouteAddress: s.Inet6RouteAddress,
|
||||
Inet4RouteExcludeAddress: s.Inet4RouteExcludeAddress,
|
||||
Inet6RouteExcludeAddress: s.Inet6RouteExcludeAddress,
|
||||
Logger: &singLogger{
|
||||
tag: "tun",
|
||||
zapLogger: s.Logger,
|
||||
},
|
||||
}
|
||||
tunIf, err := tun.New(tunOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tun interface: %w", err)
|
||||
}
|
||||
defer tunIf.Close()
|
||||
|
||||
tunStack, err := tun.NewSystem(tun.StackOptions{
|
||||
Context: context.Background(),
|
||||
Tun: tunIf,
|
||||
TunOptions: tunOpts,
|
||||
UDPTimeout: s.Timeout,
|
||||
Handler: &tunHandler{s},
|
||||
Logger: &singLogger{
|
||||
tag: "tun-stack",
|
||||
zapLogger: s.Logger,
|
||||
},
|
||||
ForwarderBindInterface: true,
|
||||
InterfaceFinder: &interfaceFinder{},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tun stack: %w", err)
|
||||
}
|
||||
defer tunStack.Close()
|
||||
return tunStack.(tun.StackRunner).Run()
|
||||
}
|
||||
|
||||
type tunHandler struct {
|
||||
*Server
|
||||
}
|
||||
|
||||
var _ tun.Handler = (*tunHandler)(nil)
|
||||
|
||||
func (t *tunHandler) NewConnection(ctx context.Context, conn net.Conn, m metadata.Metadata) error {
|
||||
addr := m.Source.String()
|
||||
reqAddr := m.Destination.String()
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.TCPRequest(addr, reqAddr)
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.TCPError(addr, reqAddr, closeErr)
|
||||
}
|
||||
}()
|
||||
rc, err := t.HyClient.TCP(reqAddr)
|
||||
if err != nil {
|
||||
closeErr = err
|
||||
// the returned err is ignored by caller
|
||||
return nil
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// start forwarding
|
||||
copyErrChan := make(chan error, 3)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
copyErrChan <- ctx.Err()
|
||||
}()
|
||||
go func() {
|
||||
_, copyErr := io.Copy(rc, conn)
|
||||
copyErrChan <- copyErr
|
||||
}()
|
||||
go func() {
|
||||
_, copyErr := io.Copy(conn, rc)
|
||||
copyErrChan <- copyErr
|
||||
}()
|
||||
closeErr = <-copyErrChan
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tunHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, m metadata.Metadata) error {
|
||||
addr := m.Source.String()
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.UDPRequest(addr)
|
||||
}
|
||||
var closeErr error
|
||||
defer func() {
|
||||
if t.EventLogger != nil {
|
||||
t.EventLogger.UDPError(addr, closeErr)
|
||||
}
|
||||
}()
|
||||
rc, err := t.HyClient.UDP()
|
||||
if err != nil {
|
||||
closeErr = err
|
||||
// the returned err is simply called into NewError again
|
||||
return nil
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// start forwarding
|
||||
copyErrChan := make(chan error, 3)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
copyErrChan <- ctx.Err()
|
||||
}()
|
||||
// local <- remote
|
||||
go func() {
|
||||
for {
|
||||
bs, from, err := rc.Receive()
|
||||
if err != nil {
|
||||
copyErrChan <- err
|
||||
return
|
||||
}
|
||||
var fromAddr metadata.Socksaddr
|
||||
if ap, perr := netip.ParseAddrPort(from); perr == nil {
|
||||
fromAddr = metadata.SocksaddrFromNetIP(ap)
|
||||
} else {
|
||||
fromAddr.Fqdn = from
|
||||
}
|
||||
err = conn.WritePacket(buf.As(bs), fromAddr)
|
||||
if err != nil {
|
||||
copyErrChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// local -> remote
|
||||
go func() {
|
||||
buffer := buf.NewPacket()
|
||||
defer buffer.Release()
|
||||
|
||||
for {
|
||||
buffer.Reset()
|
||||
addr, err := conn.ReadPacket(buffer)
|
||||
if err != nil {
|
||||
copyErrChan <- err
|
||||
return
|
||||
}
|
||||
err = rc.Send(buffer.Bytes(), addr.String())
|
||||
if err != nil {
|
||||
copyErrChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
closeErr = <-copyErrChan
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tunHandler) NewError(ctx context.Context, err error) {
|
||||
// unused
|
||||
}
|
||||
|
||||
type interfaceFinder struct{}
|
||||
|
||||
var _ control.InterfaceFinder = (*interfaceFinder)(nil)
|
||||
|
||||
func (f *interfaceFinder) InterfaceIndexByName(name string) (int, error) {
|
||||
ifce, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return ifce.Index, nil
|
||||
}
|
||||
|
||||
func (f *interfaceFinder) InterfaceNameByIndex(index int) (string, error) {
|
||||
ifce, err := net.InterfaceByIndex(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ifce.Name, nil
|
||||
}
|
1270
app/internal/url/url.go
Normal file
1270
app/internal/url/url.go
Normal file
File diff suppressed because it is too large
Load diff
91
app/internal/url/url_test.go
Normal file
91
app/internal/url/url_test.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package url
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
type args struct {
|
||||
rawURL string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no port",
|
||||
args: args{
|
||||
rawURL: "hysteria2://ganggang@icecreamsogood/",
|
||||
},
|
||||
want: &URL{
|
||||
Scheme: "hysteria2",
|
||||
User: User("ganggang"),
|
||||
Host: "icecreamsogood",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single port",
|
||||
args: args{
|
||||
rawURL: "hysteria2://yesyes@icecreamsogood:8888/",
|
||||
},
|
||||
want: &URL{
|
||||
Scheme: "hysteria2",
|
||||
User: User("yesyes"),
|
||||
Host: "icecreamsogood:8888",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multi port",
|
||||
args: args{
|
||||
rawURL: "hysteria2://darkness@laplus.org:8888,9999,11111/",
|
||||
},
|
||||
want: &URL{
|
||||
Scheme: "hysteria2",
|
||||
User: User("darkness"),
|
||||
Host: "laplus.org:8888,9999,11111",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "range port",
|
||||
args: args{
|
||||
rawURL: "hysteria2://darkness@laplus.org:8888-9999/",
|
||||
},
|
||||
want: &URL{
|
||||
Scheme: "hysteria2",
|
||||
User: User("darkness"),
|
||||
Host: "laplus.org:8888-9999",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both",
|
||||
args: args{
|
||||
rawURL: "hysteria2://gawr:gura@atlantis.moe:443,7788-8899,10010/",
|
||||
},
|
||||
want: &URL{
|
||||
Scheme: "hysteria2",
|
||||
User: UserPassword("gawr", "gura"),
|
||||
Host: "atlantis.moe:443,7788-8899,10010",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Parse(tt.args.rawURL)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Parse() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
68
app/internal/utils/bpsconv.go
Normal file
68
app/internal/utils/bpsconv.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Byte = 1
|
||||
Kilobyte = Byte * 1000
|
||||
Megabyte = Kilobyte * 1000
|
||||
Gigabyte = Megabyte * 1000
|
||||
Terabyte = Gigabyte * 1000
|
||||
)
|
||||
|
||||
// StringToBps converts a string to a bandwidth value in bytes per second.
|
||||
// E.g. "100 Mbps", "512 kbps", "1g" are all valid.
|
||||
func StringToBps(s string) (uint64, error) {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
spl := 0
|
||||
for i, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
spl = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if spl == 0 {
|
||||
// No unit or no value
|
||||
return 0, errors.New("invalid format")
|
||||
}
|
||||
v, err := strconv.ParseUint(s[:spl], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
unit := strings.TrimSpace(s[spl:])
|
||||
|
||||
switch strings.ToLower(unit) {
|
||||
case "b", "bps":
|
||||
return v * Byte / 8, nil
|
||||
case "k", "kb", "kbps":
|
||||
return v * Kilobyte / 8, nil
|
||||
case "m", "mb", "mbps":
|
||||
return v * Megabyte / 8, nil
|
||||
case "g", "gb", "gbps":
|
||||
return v * Gigabyte / 8, nil
|
||||
case "t", "tb", "tbps":
|
||||
return v * Terabyte / 8, nil
|
||||
default:
|
||||
return 0, errors.New("unsupported unit")
|
||||
}
|
||||
}
|
||||
|
||||
// ConvBandwidth handles both string and int types for bandwidth.
|
||||
// When using string, it will be parsed as a bandwidth string with units.
|
||||
// When using int, it will be parsed as a raw bandwidth in bytes per second.
|
||||
// It does NOT support float types.
|
||||
func ConvBandwidth(bw interface{}) (uint64, error) {
|
||||
switch bwT := bw.(type) {
|
||||
case string:
|
||||
return StringToBps(bwT)
|
||||
case int:
|
||||
return uint64(bwT), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid type %T for bandwidth", bwT)
|
||||
}
|
||||
}
|
40
app/internal/utils/bpsconv_test.go
Normal file
40
app/internal/utils/bpsconv_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStringToBps(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want uint64
|
||||
wantErr bool
|
||||
}{
|
||||
{"bps", args{"800 bps"}, 100, false},
|
||||
{"kbps", args{"800 kbps"}, 100_000, false},
|
||||
{"mbps", args{"800 mbps"}, 100_000_000, false},
|
||||
{"gbps", args{"800 gbps"}, 100_000_000_000, false},
|
||||
{"tbps", args{"800 tbps"}, 100_000_000_000_000, false},
|
||||
{"mbps simp", args{"100m"}, 12_500_000, false},
|
||||
{"gbps simp upper", args{"2G"}, 250_000_000, false},
|
||||
{"invalid 1", args{"damn"}, 0, true},
|
||||
{"invalid 2", args{"6444"}, 0, true},
|
||||
{"invalid 3", args{"5.4 mbps"}, 0, true},
|
||||
{"invalid 4", args{"kbps"}, 0, true},
|
||||
{"invalid 5", args{"1234 5678 gbps"}, 0, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := StringToBps(tt.args.s)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("StringToBps() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("StringToBps() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
198
app/internal/utils/certloader.go
Normal file
198
app/internal/utils/certloader.go
Normal file
|
@ -0,0 +1,198 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LocalCertificateLoader struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
SNIGuard SNIGuardFunc
|
||||
|
||||
lock sync.Mutex
|
||||
cache atomic.Pointer[localCertificateCache]
|
||||
}
|
||||
|
||||
type SNIGuardFunc func(info *tls.ClientHelloInfo, cert *tls.Certificate) error
|
||||
|
||||
// localCertificateCache holds the certificate and its mod times.
|
||||
// this struct is designed to be read-only.
|
||||
//
|
||||
// to update the cache, use LocalCertificateLoader.makeCache and
|
||||
// update the LocalCertificateLoader.cache field.
|
||||
type localCertificateCache struct {
|
||||
certificate *tls.Certificate
|
||||
certModTime time.Time
|
||||
keyModTime time.Time
|
||||
}
|
||||
|
||||
func (l *LocalCertificateLoader) InitializeCache() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
cache, err := l.makeCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.cache.Store(cache)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LocalCertificateLoader) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := l.getCertificateWithCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if l.SNIGuard == nil {
|
||||
return cert, nil
|
||||
}
|
||||
err = l.SNIGuard(info, cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (l *LocalCertificateLoader) checkModTime() (certModTime, keyModTime time.Time, err error) {
|
||||
fi, err := os.Stat(l.CertFile)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to stat certificate file: %w", err)
|
||||
return
|
||||
}
|
||||
certModTime = fi.ModTime()
|
||||
|
||||
fi, err = os.Stat(l.KeyFile)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to stat key file: %w", err)
|
||||
return
|
||||
}
|
||||
keyModTime = fi.ModTime()
|
||||
return
|
||||
}
|
||||
|
||||
func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) {
|
||||
c := &localCertificateCache{}
|
||||
|
||||
c.certModTime, c.keyModTime, err = l.checkModTime()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.certificate = &cert
|
||||
if c.certificate.Leaf == nil {
|
||||
// certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23
|
||||
c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cache = c
|
||||
return
|
||||
}
|
||||
|
||||
func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) {
|
||||
cache := l.cache.Load()
|
||||
|
||||
certModTime, keyModTime, terr := l.checkModTime()
|
||||
if terr != nil {
|
||||
if cache != nil {
|
||||
// use cache when file is temporarily unavailable
|
||||
return cache.certificate, nil
|
||||
}
|
||||
return nil, terr
|
||||
}
|
||||
|
||||
if cache != nil && cache.certModTime.Equal(certModTime) && cache.keyModTime.Equal(keyModTime) {
|
||||
// cache is up-to-date
|
||||
return cache.certificate, nil
|
||||
}
|
||||
|
||||
if cache != nil {
|
||||
if !l.lock.TryLock() {
|
||||
// another goroutine is updating the cache
|
||||
return cache.certificate, nil
|
||||
}
|
||||
} else {
|
||||
l.lock.Lock()
|
||||
}
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.cache.Load() != cache {
|
||||
// another goroutine updated the cache
|
||||
return l.cache.Load().certificate, nil
|
||||
}
|
||||
|
||||
newCache, err := l.makeCache()
|
||||
if err != nil {
|
||||
if cache != nil {
|
||||
// use cache when loading failed
|
||||
return cache.certificate, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.cache.Store(newCache)
|
||||
return newCache.certificate, nil
|
||||
}
|
||||
|
||||
// getNameFromClientHello returns a normalized form of hello.ServerName.
|
||||
// If hello.ServerName is empty (i.e. client did not use SNI), then the
|
||||
// associated connection's local address is used to extract an IP address.
|
||||
//
|
||||
// ref: https://github.com/caddyserver/certmagic/blob/3bad5b6bb595b09c14bd86ff0b365d302faaf5e2/handshake.go#L838
|
||||
func getNameFromClientHello(hello *tls.ClientHelloInfo) string {
|
||||
normalizedName := func(serverName string) string {
|
||||
return strings.ToLower(strings.TrimSpace(serverName))
|
||||
}
|
||||
localIPFromConn := func(c net.Conn) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
localAddr := c.LocalAddr().String()
|
||||
ip, _, err := net.SplitHostPort(localAddr)
|
||||
if err != nil {
|
||||
ip = localAddr
|
||||
}
|
||||
if scopeIDStart := strings.Index(ip, "%"); scopeIDStart > -1 {
|
||||
ip = ip[:scopeIDStart]
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
if name := normalizedName(hello.ServerName); name != "" {
|
||||
return name
|
||||
}
|
||||
return localIPFromConn(hello.Conn)
|
||||
}
|
||||
|
||||
func SNIGuardDNSSAN(info *tls.ClientHelloInfo, cert *tls.Certificate) error {
|
||||
if len(cert.Leaf.DNSNames) == 0 {
|
||||
return nil
|
||||
}
|
||||
return SNIGuardStrict(info, cert)
|
||||
}
|
||||
|
||||
func SNIGuardStrict(info *tls.ClientHelloInfo, cert *tls.Certificate) error {
|
||||
hostname := getNameFromClientHello(info)
|
||||
err := cert.Leaf.VerifyHostname(hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sni guard: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
139
app/internal/utils/certloader_test.go
Normal file
139
app/internal/utils/certloader_test.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
testListen = "127.82.39.147:12947"
|
||||
testCAFile = "./testcerts/ca"
|
||||
testCertFile = "./testcerts/cert"
|
||||
testKeyFile = "./testcerts/key"
|
||||
)
|
||||
|
||||
func TestCertificateLoaderPathError(t *testing.T) {
|
||||
assert.NoError(t, os.RemoveAll(testCertFile))
|
||||
assert.NoError(t, os.RemoveAll(testKeyFile))
|
||||
loader := LocalCertificateLoader{
|
||||
CertFile: testCertFile,
|
||||
KeyFile: testKeyFile,
|
||||
SNIGuard: SNIGuardStrict,
|
||||
}
|
||||
err := loader.InitializeCache()
|
||||
var pathErr *os.PathError
|
||||
assert.ErrorAs(t, err, &pathErr)
|
||||
}
|
||||
|
||||
func TestCertificateLoaderFullChain(t *testing.T) {
|
||||
assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain"))
|
||||
|
||||
loader := LocalCertificateLoader{
|
||||
CertFile: testCertFile,
|
||||
KeyFile: testKeyFile,
|
||||
SNIGuard: SNIGuardStrict,
|
||||
}
|
||||
assert.NoError(t, loader.InitializeCache())
|
||||
|
||||
lis, err := tls.Listen("tcp", testListen, &tls.Config{
|
||||
GetCertificate: loader.GetCertificate,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
defer lis.Close()
|
||||
go http.Serve(lis, nil)
|
||||
|
||||
assert.Error(t, runTestTLSClient("unmatched-sni.example.com"))
|
||||
assert.Error(t, runTestTLSClient(""))
|
||||
assert.NoError(t, runTestTLSClient("example.com"))
|
||||
}
|
||||
|
||||
func TestCertificateLoaderNoSAN(t *testing.T) {
|
||||
assert.NoError(t, generateTestCertificate(nil, "selfsign"))
|
||||
|
||||
loader := LocalCertificateLoader{
|
||||
CertFile: testCertFile,
|
||||
KeyFile: testKeyFile,
|
||||
SNIGuard: SNIGuardDNSSAN,
|
||||
}
|
||||
assert.NoError(t, loader.InitializeCache())
|
||||
|
||||
lis, err := tls.Listen("tcp", testListen, &tls.Config{
|
||||
GetCertificate: loader.GetCertificate,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
defer lis.Close()
|
||||
go http.Serve(lis, nil)
|
||||
|
||||
assert.NoError(t, runTestTLSClient(""))
|
||||
}
|
||||
|
||||
func TestCertificateLoaderReplaceCertificate(t *testing.T) {
|
||||
assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain"))
|
||||
|
||||
loader := LocalCertificateLoader{
|
||||
CertFile: testCertFile,
|
||||
KeyFile: testKeyFile,
|
||||
SNIGuard: SNIGuardStrict,
|
||||
}
|
||||
assert.NoError(t, loader.InitializeCache())
|
||||
|
||||
lis, err := tls.Listen("tcp", testListen, &tls.Config{
|
||||
GetCertificate: loader.GetCertificate,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
defer lis.Close()
|
||||
go http.Serve(lis, nil)
|
||||
|
||||
assert.NoError(t, runTestTLSClient("example.com"))
|
||||
assert.Error(t, runTestTLSClient("2.example.com"))
|
||||
|
||||
assert.NoError(t, generateTestCertificate([]string{"2.example.com"}, "fullchain"))
|
||||
|
||||
assert.Error(t, runTestTLSClient("example.com"))
|
||||
assert.NoError(t, runTestTLSClient("2.example.com"))
|
||||
}
|
||||
|
||||
func generateTestCertificate(dnssan []string, certType string) error {
|
||||
args := []string{
|
||||
"certloader_test_gencert.py",
|
||||
"--ca", testCAFile,
|
||||
"--cert", testCertFile,
|
||||
"--key", testKeyFile,
|
||||
"--type", certType,
|
||||
}
|
||||
if len(dnssan) > 0 {
|
||||
args = append(args, "--dnssan", strings.Join(dnssan, ","))
|
||||
}
|
||||
cmd := exec.Command("python", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate test certificate: %s", out)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runTestTLSClient(sni string) error {
|
||||
args := []string{
|
||||
"certloader_test_tlsclient.py",
|
||||
"--server", testListen,
|
||||
"--ca", testCAFile,
|
||||
}
|
||||
if sni != "" {
|
||||
args = append(args, "--sni", sni)
|
||||
}
|
||||
cmd := exec.Command("python", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("Failed to run test TLS client: %s", out)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
134
app/internal/utils/certloader_test_gencert.py
Normal file
134
app/internal/utils/certloader_test_gencert.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
import argparse
|
||||
import datetime
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
|
||||
|
||||
|
||||
def create_key():
|
||||
return ec.generate_private_key(ec.SECP256R1())
|
||||
|
||||
|
||||
def create_certificate(cert_type, subject, issuer, private_key, public_key, dns_san=None):
|
||||
serial_number = x509.random_serial_number()
|
||||
not_valid_before = datetime.datetime.now(datetime.UTC)
|
||||
not_valid_after = not_valid_before + datetime.timedelta(days=365)
|
||||
|
||||
subject_name = x509.Name([
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, subject.get('C', 'ZZ')),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject.get('O', 'No Organization')),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, subject.get('CN', 'No CommonName')),
|
||||
])
|
||||
issuer_name = x509.Name([
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, issuer.get('C', 'ZZ')),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, issuer.get('O', 'No Organization')),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, issuer.get('CN', 'No CommonName')),
|
||||
])
|
||||
builder = x509.CertificateBuilder()
|
||||
builder = builder.subject_name(subject_name)
|
||||
builder = builder.issuer_name(issuer_name)
|
||||
builder = builder.public_key(public_key)
|
||||
builder = builder.serial_number(serial_number)
|
||||
builder = builder.not_valid_before(not_valid_before)
|
||||
builder = builder.not_valid_after(not_valid_after)
|
||||
if cert_type == 'root':
|
||||
builder = builder.add_extension(
|
||||
x509.BasicConstraints(ca=True, path_length=None), critical=True
|
||||
)
|
||||
elif cert_type == 'intermediate':
|
||||
builder = builder.add_extension(
|
||||
x509.BasicConstraints(ca=True, path_length=0), critical=True
|
||||
)
|
||||
elif cert_type == 'leaf':
|
||||
builder = builder.add_extension(
|
||||
x509.BasicConstraints(ca=False, path_length=None), critical=True
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Invalid cert_type: {cert_type}')
|
||||
if dns_san:
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName([x509.DNSName(d) for d in dns_san.split(',')]),
|
||||
critical=False
|
||||
)
|
||||
return builder.sign(private_key=private_key, algorithm=hashes.SHA256())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate HTTPS server certificate.')
|
||||
parser.add_argument('--ca', required=True,
|
||||
help='Path to write the X509 CA certificate in PEM format')
|
||||
parser.add_argument('--cert', required=True,
|
||||
help='Path to write the X509 certificate in PEM format')
|
||||
parser.add_argument('--key', required=True,
|
||||
help='Path to write the private key in PEM format')
|
||||
parser.add_argument('--dnssan', required=False, default=None,
|
||||
help='Comma-separated list of DNS SANs')
|
||||
parser.add_argument('--type', required=True, choices=['selfsign', 'fullchain'],
|
||||
help='Type of certificate to generate')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
key = create_key()
|
||||
public_key = key.public_key()
|
||||
|
||||
if args.type == 'selfsign':
|
||||
subject = {"C": "ZZ", "O": "Certificate", "CN": "Certificate"}
|
||||
cert = create_certificate(
|
||||
cert_type='root',
|
||||
subject=subject,
|
||||
issuer=subject,
|
||||
private_key=key,
|
||||
public_key=public_key,
|
||||
dns_san=args.dnssan)
|
||||
with open(args.ca, 'wb') as f:
|
||||
f.write(cert.public_bytes(Encoding.PEM))
|
||||
with open(args.cert, 'wb') as f:
|
||||
f.write(cert.public_bytes(Encoding.PEM))
|
||||
with open(args.key, 'wb') as f:
|
||||
f.write(
|
||||
key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
|
||||
|
||||
elif args.type == 'fullchain':
|
||||
ca_key = create_key()
|
||||
ca_public_key = ca_key.public_key()
|
||||
ca_subject = {"C": "ZZ", "O": "Root CA", "CN": "Root CA"}
|
||||
ca_cert = create_certificate(
|
||||
cert_type='root',
|
||||
subject=ca_subject,
|
||||
issuer=ca_subject,
|
||||
private_key=ca_key,
|
||||
public_key=ca_public_key)
|
||||
|
||||
intermediate_key = create_key()
|
||||
intermediate_public_key = intermediate_key.public_key()
|
||||
intermediate_subject = {"C": "ZZ", "O": "Intermediate CA", "CN": "Intermediate CA"}
|
||||
intermediate_cert = create_certificate(
|
||||
cert_type='intermediate',
|
||||
subject=intermediate_subject,
|
||||
issuer=ca_subject,
|
||||
private_key=ca_key,
|
||||
public_key=intermediate_public_key)
|
||||
|
||||
leaf_subject = {"C": "ZZ", "O": "Leaf Certificate", "CN": "Leaf Certificate"}
|
||||
cert = create_certificate(
|
||||
cert_type='leaf',
|
||||
subject=leaf_subject,
|
||||
issuer=intermediate_subject,
|
||||
private_key=intermediate_key,
|
||||
public_key=public_key,
|
||||
dns_san=args.dnssan)
|
||||
|
||||
with open(args.ca, 'wb') as f:
|
||||
f.write(ca_cert.public_bytes(Encoding.PEM))
|
||||
with open(args.cert, 'wb') as f:
|
||||
f.write(cert.public_bytes(Encoding.PEM))
|
||||
f.write(intermediate_cert.public_bytes(Encoding.PEM))
|
||||
with open(args.key, 'wb') as f:
|
||||
f.write(
|
||||
key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
60
app/internal/utils/certloader_test_tlsclient.py
Normal file
60
app/internal/utils/certloader_test_tlsclient.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
import argparse
|
||||
import ssl
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
def check_tls(server, ca_cert, sni, alpn):
|
||||
try:
|
||||
host, port = server.split(":")
|
||||
port = int(port)
|
||||
|
||||
if ca_cert:
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_cert)
|
||||
context.check_hostname = sni is not None
|
||||
context.verify_mode = ssl.CERT_REQUIRED
|
||||
else:
|
||||
context = ssl.create_default_context()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if alpn:
|
||||
context.set_alpn_protocols([p for p in alpn.split(",")])
|
||||
|
||||
with socket.create_connection((host, port)) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=sni) as ssock:
|
||||
# Verify handshake and certificate
|
||||
print(f'Connected to {ssock.version()} using {ssock.cipher()}')
|
||||
print(f'Server certificate validated and details: {ssock.getpeercert()}')
|
||||
print("OK")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Test TLS Server")
|
||||
parser.add_argument("--server", required=True,
|
||||
help="Server address to test (e.g., 127.1.2.3:8443)")
|
||||
parser.add_argument("--ca", required=False, default=None,
|
||||
help="CA certificate file used to validate the server certificate"
|
||||
"Omit to use insecure connection")
|
||||
parser.add_argument("--sni", required=False, default=None,
|
||||
help="SNI to send in ClientHello")
|
||||
parser.add_argument("--alpn", required=False, default='h2',
|
||||
help="ALPN to send in ClientHello")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
exit_status = check_tls(
|
||||
server=args.server,
|
||||
ca_cert=args.ca,
|
||||
sni=args.sni,
|
||||
alpn=args.alpn)
|
||||
|
||||
sys.exit(exit_status)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
172
app/internal/utils/geoloader.go
Normal file
172
app/internal/utils/geoloader.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/extras/v2/outbounds/acl"
|
||||
"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
|
||||
)
|
||||
|
||||
const (
|
||||
geoipFilename = "geoip.dat"
|
||||
geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
|
||||
geositeFilename = "geosite.dat"
|
||||
geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
|
||||
geoDlTmpPattern = ".hysteria-geoloader.dlpart.*"
|
||||
|
||||
geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days
|
||||
)
|
||||
|
||||
var _ acl.GeoLoader = (*GeoLoader)(nil)
|
||||
|
||||
// GeoLoader provides the on-demand GeoIP/GeoSite database
|
||||
// loading functionality required by the ACL engine.
|
||||
// Empty filenames = automatic download from built-in URLs.
|
||||
type GeoLoader struct {
|
||||
GeoIPFilename string
|
||||
GeoSiteFilename string
|
||||
UpdateInterval time.Duration
|
||||
|
||||
DownloadFunc func(filename, url string)
|
||||
DownloadErrFunc func(err error)
|
||||
|
||||
geoipMap map[string]*v2geo.GeoIP
|
||||
geositeMap map[string]*v2geo.GeoSite
|
||||
}
|
||||
|
||||
func (l *GeoLoader) shouldDownload(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
// empty files are loadable by v2geo, but we consider it broken
|
||||
return true
|
||||
}
|
||||
dt := time.Now().Sub(info.ModTime())
|
||||
if l.UpdateInterval == 0 {
|
||||
return dt > geoDefaultUpdateInterval
|
||||
} else {
|
||||
return dt > l.UpdateInterval
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GeoLoader) downloadAndCheck(filename, url string, checkFunc func(filename string) error) error {
|
||||
l.DownloadFunc(filename, url)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
l.DownloadErrFunc(err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
f, err := os.CreateTemp(".", geoDlTmpPattern)
|
||||
if err != nil {
|
||||
l.DownloadErrFunc(err)
|
||||
return err
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
l.DownloadErrFunc(err)
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
|
||||
err = checkFunc(f.Name())
|
||||
if err != nil {
|
||||
l.DownloadErrFunc(fmt.Errorf("integrity check failed: %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Rename(f.Name(), filename)
|
||||
if err != nil {
|
||||
l.DownloadErrFunc(fmt.Errorf("rename failed: %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {
|
||||
if l.geoipMap != nil {
|
||||
return l.geoipMap, nil
|
||||
}
|
||||
autoDL := false
|
||||
filename := l.GeoIPFilename
|
||||
if filename == "" {
|
||||
autoDL = true
|
||||
filename = geoipFilename
|
||||
}
|
||||
if autoDL {
|
||||
if !l.shouldDownload(filename) {
|
||||
m, err := v2geo.LoadGeoIP(filename)
|
||||
if err == nil {
|
||||
l.geoipMap = m
|
||||
return m, nil
|
||||
}
|
||||
// file is broken, download it again
|
||||
}
|
||||
err := l.downloadAndCheck(filename, geoipURL, func(filename string) error {
|
||||
_, err := v2geo.LoadGeoIP(filename)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
// as long as the previous download exists, fallback to it
|
||||
if _, serr := os.Stat(filename); os.IsNotExist(serr) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
m, err := v2geo.LoadGeoIP(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.geoipMap = m
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (l *GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
|
||||
if l.geositeMap != nil {
|
||||
return l.geositeMap, nil
|
||||
}
|
||||
autoDL := false
|
||||
filename := l.GeoSiteFilename
|
||||
if filename == "" {
|
||||
autoDL = true
|
||||
filename = geositeFilename
|
||||
}
|
||||
if autoDL {
|
||||
if !l.shouldDownload(filename) {
|
||||
m, err := v2geo.LoadGeoSite(filename)
|
||||
if err == nil {
|
||||
l.geositeMap = m
|
||||
return m, nil
|
||||
}
|
||||
// file is broken, download it again
|
||||
}
|
||||
err := l.downloadAndCheck(filename, geositeURL, func(filename string) error {
|
||||
_, err := v2geo.LoadGeoSite(filename)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
// as long as the previous download exists, fallback to it
|
||||
if _, serr := os.Stat(filename); os.IsNotExist(serr) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
m, err := v2geo.LoadGeoSite(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.geositeMap = m
|
||||
return m, nil
|
||||
}
|
16
app/internal/utils/qr.go
Normal file
16
app/internal/utils/qr.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
)
|
||||
|
||||
func PrintQR(str string) {
|
||||
qrterminal.GenerateWithConfig(str, qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
Writer: os.Stdout,
|
||||
BlackChar: qrterminal.BLACK,
|
||||
WhiteChar: qrterminal.WHITE,
|
||||
})
|
||||
}
|
3
app/internal/utils/testcerts/.gitignore
vendored
Normal file
3
app/internal/utils/testcerts/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# This directory is used for certificate generation in certloader_test.go
|
||||
/*
|
||||
!/.gitignore
|
96
app/internal/utils/update.go
Normal file
96
app/internal/utils/update.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
const (
|
||||
updateCheckEndpoint = "https://api.hy2.io/v1/update"
|
||||
updateCheckTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type UpdateChecker struct {
|
||||
CurrentVersion string
|
||||
Platform string
|
||||
Architecture string
|
||||
Channel string
|
||||
Side string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func NewServerUpdateChecker(currentVersion, platform, architecture, channel string) *UpdateChecker {
|
||||
return &UpdateChecker{
|
||||
CurrentVersion: currentVersion,
|
||||
Platform: platform,
|
||||
Architecture: architecture,
|
||||
Channel: channel,
|
||||
Side: "server",
|
||||
Client: &http.Client{
|
||||
Timeout: updateCheckTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientUpdateChecker ensures that update checks are routed through a HyClient,
|
||||
// not being sent directly. This safeguard is CRITICAL, especially in scenarios where
|
||||
// users use Hysteria to bypass censorship. Making direct HTTPS requests to the API
|
||||
// endpoint could be easily spotted by censors (through SNI, for example), and could
|
||||
// serve as a signal to identify and penalize Hysteria users.
|
||||
func NewClientUpdateChecker(currentVersion, platform, architecture, channel string, hyClient client.Client) *UpdateChecker {
|
||||
return &UpdateChecker{
|
||||
CurrentVersion: currentVersion,
|
||||
Platform: platform,
|
||||
Architecture: architecture,
|
||||
Channel: channel,
|
||||
Side: "client",
|
||||
Client: &http.Client{
|
||||
Timeout: updateCheckTimeout,
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, network, addr string) (net.Conn, error) {
|
||||
// Unfortunately HyClient doesn't support context for now
|
||||
return hyClient.TCP(addr)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateResponse struct {
|
||||
HasUpdate bool `json:"update"`
|
||||
LatestVersion string `json:"lver"`
|
||||
URL string `json:"url"`
|
||||
Urgent bool `json:"urgent"`
|
||||
}
|
||||
|
||||
func (uc *UpdateChecker) Check() (*UpdateResponse, error) {
|
||||
url := fmt.Sprintf("%s?cver=%s&plat=%s&arch=%s&chan=%s&side=%s",
|
||||
updateCheckEndpoint,
|
||||
uc.CurrentVersion,
|
||||
uc.Platform,
|
||||
uc.Architecture,
|
||||
uc.Channel,
|
||||
uc.Side,
|
||||
)
|
||||
resp, err := uc.Client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
var uResp UpdateResponse
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
if err := decoder.Decode(&uResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &uResp, nil
|
||||
}
|
106
app/internal/utils_test/mock.go
Normal file
106
app/internal/utils_test/mock.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package utils_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
)
|
||||
|
||||
type MockEchoHyClient struct{}
|
||||
|
||||
func (c *MockEchoHyClient) TCP(addr string) (net.Conn, error) {
|
||||
return &mockEchoTCPConn{
|
||||
BufChan: make(chan []byte, 10),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *MockEchoHyClient) UDP() (client.HyUDPConn, error) {
|
||||
return &mockEchoUDPConn{
|
||||
BufChan: make(chan mockEchoUDPPacket, 10),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *MockEchoHyClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockEchoTCPConn struct {
|
||||
BufChan chan []byte
|
||||
}
|
||||
|
||||
func (c *mockEchoTCPConn) Read(b []byte) (n int, err error) {
|
||||
buf := <-c.BufChan
|
||||
if buf == nil {
|
||||
// EOF
|
||||
return 0, io.EOF
|
||||
}
|
||||
return copy(b, buf), nil
|
||||
}
|
||||
|
||||
func (c *mockEchoTCPConn) Write(b []byte) (n int, err error) {
|
||||
c.BufChan <- b
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *mockEchoTCPConn) Close() error {
|
||||
close(c.BufChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockEchoTCPConn) LocalAddr() net.Addr {
|
||||
// Not implemented
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockEchoTCPConn) RemoteAddr() net.Addr {
|
||||
// Not implemented
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockEchoTCPConn) SetDeadline(t time.Time) error {
|
||||
// Not implemented
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockEchoTCPConn) SetReadDeadline(t time.Time) error {
|
||||
// Not implemented
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockEchoTCPConn) SetWriteDeadline(t time.Time) error {
|
||||
// Not implemented
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockEchoUDPPacket struct {
|
||||
Data []byte
|
||||
Addr string
|
||||
}
|
||||
|
||||
type mockEchoUDPConn struct {
|
||||
BufChan chan mockEchoUDPPacket
|
||||
}
|
||||
|
||||
func (c *mockEchoUDPConn) Receive() ([]byte, string, error) {
|
||||
p := <-c.BufChan
|
||||
if p.Data == nil {
|
||||
// EOF
|
||||
return nil, "", io.EOF
|
||||
}
|
||||
return p.Data, p.Addr, nil
|
||||
}
|
||||
|
||||
func (c *mockEchoUDPConn) Send(bytes []byte, s string) error {
|
||||
c.BufChan <- mockEchoUDPPacket{
|
||||
Data: bytes,
|
||||
Addr: s,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockEchoUDPConn) Close() error {
|
||||
close(c.BufChan)
|
||||
return nil
|
||||
}
|
7
app/main.go
Normal file
7
app/main.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "github.com/apernet/hysteria/app/v2/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
50
app/misc/socks5_test.py
Normal file
50
app/misc/socks5_test.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
import socket
|
||||
import socks
|
||||
import time
|
||||
|
||||
TARGET = "1.1.1.1"
|
||||
|
||||
|
||||
def test_tcp() -> None:
|
||||
s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080)
|
||||
|
||||
print(f"TCP - Sending HTTP request to {TARGET}")
|
||||
start = time.time()
|
||||
s.connect((TARGET, 80))
|
||||
s.send(b"GET / HTTP/1.1\r\nHost: " + TARGET.encode() + b"\r\n\r\n")
|
||||
data = s.recv(1024)
|
||||
if not data:
|
||||
print("No data received")
|
||||
elif not data.startswith(b"HTTP/1.1 "):
|
||||
print("Invalid response received")
|
||||
else:
|
||||
print("TCP test passed")
|
||||
end = time.time()
|
||||
s.close()
|
||||
|
||||
print(f"Time: {round((end - start) * 1000, 2)} ms")
|
||||
|
||||
|
||||
def test_udp() -> None:
|
||||
s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080)
|
||||
|
||||
req = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05\x62\x61\x69\x64\x75\x03\x63\x6f\x6d\x00\x00\x01\x00\x01"
|
||||
print(f"UDP - Sending DNS request to {TARGET}")
|
||||
start = time.time()
|
||||
s.sendto(req, (TARGET, 53))
|
||||
(rsp, address) = s.recvfrom(4096)
|
||||
if address[0] == TARGET and address[1] == 53 and rsp[0] == req[0] and rsp[1] == req[1]:
|
||||
print("UDP test passed")
|
||||
else:
|
||||
print("Invalid response received")
|
||||
end = time.time()
|
||||
s.close()
|
||||
|
||||
print(f"Time: {round((end - start) * 1000, 2)} ms")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_tcp()
|
||||
test_udp()
|
22
app/pprof.go
Normal file
22
app/pprof.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
//go:build pprof
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
)
|
||||
|
||||
const (
|
||||
pprofListenAddr = ":6060"
|
||||
)
|
||||
|
||||
func init() {
|
||||
fmt.Printf("!!! pprof enabled, listening on %s\n", pprofListenAddr)
|
||||
go func() {
|
||||
if err := http.ListenAndServe(pprofListenAddr, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
19
cmd/acme.go
19
cmd/acme.go
|
@ -1,19 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
)
|
||||
|
||||
func acmeTLSConfig(domains []string, email string, disableHTTP bool, disableTLSALPN bool,
|
||||
altHTTPPort int, altTLSALPNPort int) (*tls.Config, error) {
|
||||
certmagic.DefaultACME.Agreed = true
|
||||
certmagic.DefaultACME.Email = email
|
||||
certmagic.DefaultACME.DisableHTTPChallenge = disableHTTP
|
||||
certmagic.DefaultACME.DisableTLSALPNChallenge = disableTLSALPN
|
||||
certmagic.DefaultACME.AltHTTPPort = altHTTPPort
|
||||
certmagic.DefaultACME.AltTLSALPNPort = altTLSALPNPort
|
||||
cfg := certmagic.NewDefault()
|
||||
return cfg.TLSConfig(), cfg.ManageSync(context.Background(), domains)
|
||||
}
|
395
cmd/client.go
395
cmd/client.go
|
@ -1,395 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/lucas-clemente/quic-go/congestion"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tobyxdd/hysteria/pkg/acl"
|
||||
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
|
||||
"github.com/tobyxdd/hysteria/pkg/core"
|
||||
hyHTTP "github.com/tobyxdd/hysteria/pkg/http"
|
||||
"github.com/tobyxdd/hysteria/pkg/obfs"
|
||||
"github.com/tobyxdd/hysteria/pkg/relay"
|
||||
"github.com/tobyxdd/hysteria/pkg/socks5"
|
||||
"github.com/tobyxdd/hysteria/pkg/tproxy"
|
||||
"github.com/tobyxdd/hysteria/pkg/transport"
|
||||
"github.com/tobyxdd/hysteria/pkg/tun"
|
||||
)
|
||||
|
||||
func client(config *clientConfig) {
|
||||
logrus.WithField("config", config.String()).Info("Client configuration loaded")
|
||||
// Resolver
|
||||
if len(config.Resolver) > 0 {
|
||||
setResolver(config.Resolver)
|
||||
}
|
||||
// TLS
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: config.ServerName,
|
||||
InsecureSkipVerify: config.Insecure,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
if config.ALPN != "" {
|
||||
tlsConfig.NextProtos = []string{config.ALPN}
|
||||
} else {
|
||||
tlsConfig.NextProtos = []string{DefaultALPN}
|
||||
}
|
||||
// Load CA
|
||||
if len(config.CustomCA) > 0 {
|
||||
bs, err := ioutil.ReadFile(config.CustomCA)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"file": config.CustomCA,
|
||||
}).Fatal("Failed to load CA")
|
||||
}
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM(bs) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"file": config.CustomCA,
|
||||
}).Fatal("Failed to parse CA")
|
||||
}
|
||||
tlsConfig.RootCAs = cp
|
||||
}
|
||||
// QUIC config
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: config.ReceiveWindowConn,
|
||||
MaxStreamReceiveWindow: config.ReceiveWindowConn,
|
||||
InitialConnectionReceiveWindow: config.ReceiveWindow,
|
||||
MaxConnectionReceiveWindow: config.ReceiveWindow,
|
||||
KeepAlive: true,
|
||||
DisablePathMTUDiscovery: config.DisableMTUDiscovery,
|
||||
EnableDatagrams: true,
|
||||
}
|
||||
if config.ReceiveWindowConn == 0 {
|
||||
quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow
|
||||
quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow
|
||||
}
|
||||
if config.ReceiveWindow == 0 {
|
||||
quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow
|
||||
quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow
|
||||
}
|
||||
// Auth
|
||||
var auth []byte
|
||||
if len(config.Auth) > 0 {
|
||||
auth = config.Auth
|
||||
} else {
|
||||
auth = []byte(config.AuthString)
|
||||
}
|
||||
// Obfuscator
|
||||
var obfuscator core.Obfuscator
|
||||
if len(config.Obfs) > 0 {
|
||||
obfuscator = obfs.NewXPlusObfuscator([]byte(config.Obfs))
|
||||
}
|
||||
// ACL
|
||||
var aclEngine *acl.Engine
|
||||
if len(config.ACL) > 0 {
|
||||
var err error
|
||||
aclEngine, err = acl.LoadFromFile(config.ACL, transport.DefaultTransport)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"file": config.ACL,
|
||||
}).Fatal("Failed to parse ACL")
|
||||
}
|
||||
}
|
||||
// Client
|
||||
client, err := core.NewClient(config.Server, config.Protocol, auth, tlsConfig, quicConfig,
|
||||
transport.DefaultTransport, uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
|
||||
func(refBPS uint64) congestion.CongestionControl {
|
||||
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
|
||||
}, obfuscator)
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize client")
|
||||
}
|
||||
defer client.Close()
|
||||
logrus.WithField("addr", config.Server).Info("Connected")
|
||||
|
||||
// Local
|
||||
errChan := make(chan error)
|
||||
if len(config.SOCKS5.Listen) > 0 {
|
||||
go func() {
|
||||
var authFunc func(user, password string) bool
|
||||
if config.SOCKS5.User != "" && config.SOCKS5.Password != "" {
|
||||
authFunc = func(user, password string) bool {
|
||||
return config.SOCKS5.User == user && config.SOCKS5.Password == password
|
||||
}
|
||||
}
|
||||
socks5server, err := socks5.NewServer(client, transport.DefaultTransport, config.SOCKS5.Listen, authFunc,
|
||||
time.Duration(config.SOCKS5.Timeout)*time.Second, aclEngine, config.SOCKS5.DisableUDP,
|
||||
func(addr net.Addr, reqAddr string, action acl.Action, arg string) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"action": actionToString(action, arg),
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Debug("SOCKS5 TCP request")
|
||||
},
|
||||
func(addr net.Addr, reqAddr string, err error) {
|
||||
if err != io.EOF {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Info("SOCKS5 TCP error")
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Debug("SOCKS5 TCP EOF")
|
||||
}
|
||||
},
|
||||
func(addr net.Addr) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
}).Debug("SOCKS5 UDP associate")
|
||||
},
|
||||
func(addr net.Addr, err error) {
|
||||
if err != io.EOF {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"src": addr.String(),
|
||||
}).Info("SOCKS5 UDP error")
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
}).Debug("SOCKS5 UDP EOF")
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize SOCKS5 server")
|
||||
}
|
||||
logrus.WithField("addr", config.SOCKS5.Listen).Info("SOCKS5 server up and running")
|
||||
errChan <- socks5server.ListenAndServe()
|
||||
}()
|
||||
}
|
||||
|
||||
if len(config.HTTP.Listen) > 0 {
|
||||
go func() {
|
||||
var authFunc func(user, password string) bool
|
||||
if config.HTTP.User != "" && config.HTTP.Password != "" {
|
||||
authFunc = func(user, password string) bool {
|
||||
return config.HTTP.User == user && config.HTTP.Password == password
|
||||
}
|
||||
}
|
||||
proxy, err := hyHTTP.NewProxyHTTPServer(client, transport.DefaultTransport,
|
||||
time.Duration(config.HTTP.Timeout)*time.Second, aclEngine,
|
||||
func(reqAddr string, action acl.Action, arg string) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"action": actionToString(action, arg),
|
||||
"dst": reqAddr,
|
||||
}).Debug("HTTP request")
|
||||
},
|
||||
authFunc)
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize HTTP server")
|
||||
}
|
||||
if config.HTTP.Cert != "" && config.HTTP.Key != "" {
|
||||
logrus.WithField("addr", config.HTTP.Listen).Info("HTTPS server up and running")
|
||||
errChan <- http.ListenAndServeTLS(config.HTTP.Listen, config.HTTP.Cert, config.HTTP.Key, proxy)
|
||||
} else {
|
||||
logrus.WithField("addr", config.HTTP.Listen).Info("HTTP server up and running")
|
||||
errChan <- http.ListenAndServe(config.HTTP.Listen, proxy)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if len(config.TUN.Name) != 0 {
|
||||
go func() {
|
||||
timeout := time.Duration(config.TUN.Timeout) * time.Second
|
||||
if timeout == 0 {
|
||||
timeout = 300 * time.Second
|
||||
}
|
||||
tunServer, err := tun.NewServer(client, transport.DefaultTransport,
|
||||
time.Duration(config.TUN.Timeout)*time.Second,
|
||||
config.TUN.Name, config.TUN.Address, config.TUN.Gateway, config.TUN.Mask, config.TUN.DNS, config.TUN.Persist)
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize TUN server")
|
||||
}
|
||||
tunServer.RequestFunc = func(addr net.Addr, reqAddr string) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Debugf("TUN %s request", strings.ToUpper(addr.Network()))
|
||||
}
|
||||
tunServer.ErrorFunc = func(addr net.Addr, reqAddr string, err error) {
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Debugf("TUN %s EOF", strings.ToUpper(addr.Network()))
|
||||
} else if err == core.ErrClosed && strings.HasPrefix(addr.Network(), "udp") {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network()))
|
||||
} else if err.Error() == "deadline exceeded" && strings.HasPrefix(addr.Network(), "tcp") {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network()))
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Infof("TUN %s error", strings.ToUpper(addr.Network()))
|
||||
}
|
||||
}
|
||||
}
|
||||
errChan <- tunServer.ListenAndServe()
|
||||
}()
|
||||
}
|
||||
|
||||
if len(config.TCPRelay.Listen) > 0 {
|
||||
config.TCPRelays = append(config.TCPRelays, Relay{
|
||||
Listen: config.TCPRelay.Listen,
|
||||
Remote: config.TCPRelay.Remote,
|
||||
Timeout: config.TCPRelay.Timeout,
|
||||
})
|
||||
}
|
||||
|
||||
if len(config.TCPRelays) > 0 {
|
||||
for _, tcpr := range config.TCPRelays {
|
||||
go func(tcpr Relay) {
|
||||
rl, err := relay.NewTCPRelay(client, transport.DefaultTransport,
|
||||
tcpr.Listen, tcpr.Remote,
|
||||
time.Duration(tcpr.Timeout)*time.Second,
|
||||
func(addr net.Addr) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
}).Debug("TCP relay request")
|
||||
},
|
||||
func(addr net.Addr, err error) {
|
||||
if err != io.EOF {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"src": addr.String(),
|
||||
}).Info("TCP relay error")
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
}).Debug("TCP relay EOF")
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize TCP relay")
|
||||
}
|
||||
logrus.WithField("addr", tcpr.Listen).Info("TCP relay up and running")
|
||||
errChan <- rl.ListenAndServe()
|
||||
}(tcpr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.UDPRelay.Listen) > 0 {
|
||||
config.UDPRelays = append(config.UDPRelays, Relay{
|
||||
Listen: config.UDPRelay.Listen,
|
||||
Remote: config.UDPRelay.Remote,
|
||||
Timeout: config.UDPRelay.Timeout,
|
||||
})
|
||||
}
|
||||
|
||||
if len(config.UDPRelays) > 0 {
|
||||
for _, udpr := range config.UDPRelays {
|
||||
go func(udpr Relay) {
|
||||
rl, err := relay.NewUDPRelay(client, transport.DefaultTransport,
|
||||
udpr.Listen, udpr.Remote,
|
||||
time.Duration(udpr.Timeout)*time.Second,
|
||||
func(addr net.Addr) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
}).Debug("UDP relay request")
|
||||
},
|
||||
func(addr net.Addr, err error) {
|
||||
if err != relay.ErrTimeout {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"src": addr.String(),
|
||||
}).Info("UDP relay error")
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
}).Debug("UDP relay session closed")
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize UDP relay")
|
||||
}
|
||||
logrus.WithField("addr", udpr.Listen).Info("UDP relay up and running")
|
||||
errChan <- rl.ListenAndServe()
|
||||
}(udpr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.TCPTProxy.Listen) > 0 {
|
||||
go func() {
|
||||
rl, err := tproxy.NewTCPTProxy(client, transport.DefaultTransport,
|
||||
config.TCPTProxy.Listen, time.Duration(config.TCPTProxy.Timeout)*time.Second,
|
||||
func(addr, reqAddr net.Addr) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr.String(),
|
||||
}).Debug("TCP TProxy request")
|
||||
},
|
||||
func(addr, reqAddr net.Addr, err error) {
|
||||
if err != io.EOF {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr.String(),
|
||||
}).Info("TCP TProxy error")
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr.String(),
|
||||
}).Debug("TCP TProxy EOF")
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize TCP TProxy")
|
||||
}
|
||||
logrus.WithField("addr", config.TCPTProxy.Listen).Info("TCP TProxy up and running")
|
||||
errChan <- rl.ListenAndServe()
|
||||
}()
|
||||
}
|
||||
|
||||
if len(config.UDPTProxy.Listen) > 0 {
|
||||
go func() {
|
||||
rl, err := tproxy.NewUDPTProxy(client, transport.DefaultTransport,
|
||||
config.UDPTProxy.Listen, time.Duration(config.UDPTProxy.Timeout)*time.Second,
|
||||
func(addr net.Addr) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
}).Debug("UDP TProxy request")
|
||||
},
|
||||
func(addr net.Addr, err error) {
|
||||
if err != tproxy.ErrTimeout {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"src": addr.String(),
|
||||
}).Info("UDP TProxy error")
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
}).Debug("UDP TProxy session closed")
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize UDP TProxy")
|
||||
}
|
||||
logrus.WithField("addr", config.UDPTProxy.Listen).Info("UDP TProxy up and running")
|
||||
errChan <- rl.ListenAndServe()
|
||||
}()
|
||||
}
|
||||
|
||||
err = <-errChan
|
||||
logrus.WithField("error", err).Fatal("Client shutdown")
|
||||
}
|
218
cmd/config.go
218
cmd/config.go
|
@ -1,218 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yosuke-furukawa/json5/encoding/json5"
|
||||
)
|
||||
|
||||
const (
|
||||
mbpsToBps = 125000
|
||||
|
||||
DefaultStreamReceiveWindow = 15728640 // 15 MB/s
|
||||
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
|
||||
DefaultMaxIncomingStreams = 1024
|
||||
|
||||
DefaultALPN = "hysteria"
|
||||
)
|
||||
|
||||
type serverConfig struct {
|
||||
Listen string `json:"listen"`
|
||||
Protocol string `json:"protocol"`
|
||||
ACME struct {
|
||||
Domains []string `json:"domains"`
|
||||
Email string `json:"email"`
|
||||
DisableHTTPChallenge bool `json:"disable_http"`
|
||||
DisableTLSALPNChallenge bool `json:"disable_tlsalpn"`
|
||||
AltHTTPPort int `json:"alt_http_port"`
|
||||
AltTLSALPNPort int `json:"alt_tlsalpn_port"`
|
||||
} `json:"acme"`
|
||||
CertFile string `json:"cert"`
|
||||
KeyFile string `json:"key"`
|
||||
// Optional below
|
||||
UpMbps int `json:"up_mbps"`
|
||||
DownMbps int `json:"down_mbps"`
|
||||
DisableUDP bool `json:"disable_udp"`
|
||||
ACL string `json:"acl"`
|
||||
Obfs string `json:"obfs"`
|
||||
Auth struct {
|
||||
Mode string `json:"mode"`
|
||||
Config json5.RawMessage `json:"config"`
|
||||
} `json:"auth"`
|
||||
ALPN string `json:"alpn"`
|
||||
PrometheusListen string `json:"prometheus_listen"`
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn"`
|
||||
ReceiveWindowClient uint64 `json:"recv_window_client"`
|
||||
MaxConnClient int `json:"max_conn_client"`
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery"`
|
||||
IPv6Only bool `json:"ipv6_only"`
|
||||
Resolver string `json:"resolver"`
|
||||
}
|
||||
|
||||
func (c *serverConfig) Check() error {
|
||||
if len(c.Listen) == 0 {
|
||||
return errors.New("no listen address")
|
||||
}
|
||||
if len(c.ACME.Domains) == 0 && (len(c.CertFile) == 0 || len(c.KeyFile) == 0) {
|
||||
return errors.New("ACME domain or TLS cert not provided")
|
||||
}
|
||||
if c.UpMbps < 0 || c.DownMbps < 0 {
|
||||
return errors.New("invalid speed")
|
||||
}
|
||||
if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) ||
|
||||
(c.ReceiveWindowClient != 0 && c.ReceiveWindowClient < 65536) {
|
||||
return errors.New("invalid receive window size")
|
||||
}
|
||||
if c.MaxConnClient < 0 {
|
||||
return errors.New("invalid max connections per client")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConfig) String() string {
|
||||
return fmt.Sprintf("%+v", *c)
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
Listen string `json:"listen"`
|
||||
Remote string `json:"remote"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
func (r *Relay) Check() error {
|
||||
if len(r.Listen) == 0 {
|
||||
return errors.New("no relay listen address")
|
||||
}
|
||||
if len(r.Remote) == 0 {
|
||||
return errors.New("no relay remote address")
|
||||
}
|
||||
if r.Timeout != 0 && r.Timeout <= 4 {
|
||||
return errors.New("invalid relay timeout")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type clientConfig struct {
|
||||
Server string `json:"server"`
|
||||
Protocol string `json:"protocol"`
|
||||
UpMbps int `json:"up_mbps"`
|
||||
DownMbps int `json:"down_mbps"`
|
||||
// Optional below
|
||||
SOCKS5 struct {
|
||||
Listen string `json:"listen"`
|
||||
Timeout int `json:"timeout"`
|
||||
DisableUDP bool `json:"disable_udp"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
} `json:"socks5"`
|
||||
HTTP struct {
|
||||
Listen string `json:"listen"`
|
||||
Timeout int `json:"timeout"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Cert string `json:"cert"`
|
||||
Key string `json:"key"`
|
||||
} `json:"http"`
|
||||
TUN struct {
|
||||
Name string `json:"name"`
|
||||
Timeout int `json:"timeout"`
|
||||
Address string `json:"address"`
|
||||
Gateway string `json:"gateway"`
|
||||
Mask string `json:"mask"`
|
||||
DNS []string `json:"dns"`
|
||||
Persist bool `json:"persist"`
|
||||
} `json:"tun"`
|
||||
TCPRelays []Relay `json:"relay_tcps"`
|
||||
TCPRelay Relay `json:"relay_tcp"` // deprecated, but we still support it for backward compatibility
|
||||
UDPRelays []Relay `json:"relay_udps"`
|
||||
UDPRelay Relay `json:"relay_udp"` // deprecated, but we still support it for backward compatibility
|
||||
TCPTProxy struct {
|
||||
Listen string `json:"listen"`
|
||||
Timeout int `json:"timeout"`
|
||||
} `json:"tproxy_tcp"`
|
||||
UDPTProxy struct {
|
||||
Listen string `json:"listen"`
|
||||
Timeout int `json:"timeout"`
|
||||
} `json:"tproxy_udp"`
|
||||
ACL string `json:"acl"`
|
||||
Obfs string `json:"obfs"`
|
||||
Auth []byte `json:"auth"`
|
||||
AuthString string `json:"auth_str"`
|
||||
ALPN string `json:"alpn"`
|
||||
ServerName string `json:"server_name"`
|
||||
Insecure bool `json:"insecure"`
|
||||
CustomCA string `json:"ca"`
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn"`
|
||||
ReceiveWindow uint64 `json:"recv_window"`
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery"`
|
||||
Resolver string `json:"resolver"`
|
||||
}
|
||||
|
||||
func (c *clientConfig) Check() error {
|
||||
if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.TUN.Name) == 0 &&
|
||||
len(c.TCPRelay.Listen) == 0 && len(c.UDPRelay.Listen) == 0 &&
|
||||
len(c.TCPRelays) == 0 && len(c.UDPRelays) == 0 &&
|
||||
len(c.TCPTProxy.Listen) == 0 && len(c.UDPTProxy.Listen) == 0 {
|
||||
return errors.New("please enable at least one mode")
|
||||
}
|
||||
if c.SOCKS5.Timeout != 0 && c.SOCKS5.Timeout <= 4 {
|
||||
return errors.New("invalid SOCKS5 timeout")
|
||||
}
|
||||
if c.HTTP.Timeout != 0 && c.HTTP.Timeout <= 4 {
|
||||
return errors.New("invalid HTTP timeout")
|
||||
}
|
||||
if c.TUN.Timeout != 0 && c.TUN.Timeout < 4 {
|
||||
return errors.New("invalid TUN timeout")
|
||||
}
|
||||
if len(c.TCPRelay.Listen) > 0 && len(c.TCPRelay.Remote) == 0 {
|
||||
return errors.New("no TCP relay remote address")
|
||||
}
|
||||
if len(c.UDPRelay.Listen) > 0 && len(c.UDPRelay.Remote) == 0 {
|
||||
return errors.New("no UDP relay remote address")
|
||||
}
|
||||
if c.TCPRelay.Timeout != 0 && c.TCPRelay.Timeout <= 4 {
|
||||
return errors.New("invalid TCP relay timeout")
|
||||
}
|
||||
if c.UDPRelay.Timeout != 0 && c.UDPRelay.Timeout <= 4 {
|
||||
return errors.New("invalid UDP relay timeout")
|
||||
}
|
||||
for _, r := range c.TCPRelays {
|
||||
if err := r.Check(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, r := range c.UDPRelays {
|
||||
if err := r.Check(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.TCPTProxy.Timeout != 0 && c.TCPTProxy.Timeout <= 4 {
|
||||
return errors.New("invalid TCP TProxy timeout")
|
||||
}
|
||||
if c.UDPTProxy.Timeout != 0 && c.UDPTProxy.Timeout <= 4 {
|
||||
return errors.New("invalid UDP TProxy timeout")
|
||||
}
|
||||
if len(c.Server) == 0 {
|
||||
return errors.New("no server address")
|
||||
}
|
||||
if c.UpMbps <= 0 || c.DownMbps <= 0 {
|
||||
return errors.New("invalid speed")
|
||||
}
|
||||
if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) ||
|
||||
(c.ReceiveWindow != 0 && c.ReceiveWindow < 65536) {
|
||||
return errors.New("invalid receive window size")
|
||||
}
|
||||
if len(c.TCPRelay.Listen) > 0 {
|
||||
logrus.Warn("'relay_tcp' is deprecated, please use 'relay_tcps' instead")
|
||||
}
|
||||
if len(c.UDPRelay.Listen) > 0 {
|
||||
logrus.Warn("config 'relay_udp' is deprecated, please use 'relay_udps' instead")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientConfig) String() string {
|
||||
return fmt.Sprintf("%+v", *c)
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
keypairReloadInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
type keypairLoader struct {
|
||||
certMu sync.RWMutex
|
||||
cert *tls.Certificate
|
||||
certPath string
|
||||
keyPath string
|
||||
}
|
||||
|
||||
func newKeypairLoader(certPath, keyPath string) (*keypairLoader, error) {
|
||||
result := &keypairLoader{
|
||||
certPath: certPath,
|
||||
keyPath: keyPath,
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.cert = &cert
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(keypairReloadInterval)
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"cert": certPath,
|
||||
"key": keyPath,
|
||||
}).Warning("Failed to reload keypair")
|
||||
continue
|
||||
}
|
||||
result.certMu.Lock()
|
||||
result.cert = &cert
|
||||
result.certMu.Unlock()
|
||||
}
|
||||
}()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (kpr *keypairLoader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
kpr.certMu.RLock()
|
||||
defer kpr.certMu.RUnlock()
|
||||
return kpr.cert, nil
|
||||
}
|
||||
}
|
177
cmd/main.go
177
cmd/main.go
|
@ -1,177 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
nested "github.com/antonfisher/nested-logrus-formatter"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/yosuke-furukawa/json5/encoding/json5"
|
||||
)
|
||||
|
||||
var (
|
||||
appVersion = "Unknown"
|
||||
appCommit = "Unknown"
|
||||
appDate = "Unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: "Hysteria",
|
||||
Usage: "a TCP/UDP relay & SOCKS5/HTTP proxy tool optimized for poor network environments",
|
||||
Version: fmt.Sprintf("%s %s %s", appVersion, appDate, appCommit),
|
||||
Authors: []*cli.Author{{Name: "HyNetwork <https://github.com/HyNetwork>"}},
|
||||
EnableBashCompletion: true,
|
||||
Action: clientAction,
|
||||
Flags: commonFlags(),
|
||||
Before: initApp,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "Run as server mode",
|
||||
Action: serverAction,
|
||||
},
|
||||
{
|
||||
Name: "client",
|
||||
Usage: "Run as client mode",
|
||||
Action: clientAction,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func clientAction(c *cli.Context) error {
|
||||
cbs, err := ioutil.ReadFile(c.String("config"))
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"file": c.String("config"),
|
||||
"error": err,
|
||||
}).Fatal("Failed to read configuration")
|
||||
}
|
||||
// client mode
|
||||
cc, err := parseClientConfig(cbs)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"file": c.String("config"),
|
||||
"error": err,
|
||||
}).Fatal("Failed to parse client configuration")
|
||||
}
|
||||
client(cc)
|
||||
return nil
|
||||
}
|
||||
|
||||
func serverAction(c *cli.Context) error {
|
||||
cbs, err := ioutil.ReadFile(c.String("config"))
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"file": c.String("config"),
|
||||
"error": err,
|
||||
}).Fatal("Failed to read configuration")
|
||||
}
|
||||
// server mode
|
||||
sc, err := parseServerConfig(cbs)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"file": c.String("config"),
|
||||
"error": err,
|
||||
}).Fatal("Failed to parse server configuration")
|
||||
}
|
||||
server(sc)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseServerConfig(cb []byte) (*serverConfig, error) {
|
||||
var c serverConfig
|
||||
err := json5.Unmarshal(cb, &c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, c.Check()
|
||||
}
|
||||
|
||||
func parseClientConfig(cb []byte) (*clientConfig, error) {
|
||||
var c clientConfig
|
||||
err := json5.Unmarshal(cb, &c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, c.Check()
|
||||
}
|
||||
|
||||
func initApp(c *cli.Context) error {
|
||||
logrus.SetOutput(os.Stdout)
|
||||
|
||||
lvl, err := logrus.ParseLevel(c.String("log-level"))
|
||||
if err == nil {
|
||||
logrus.SetLevel(lvl)
|
||||
} else {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
if strings.ToLower(c.String("log-format")) == "json" {
|
||||
logrus.SetFormatter(&logrus.JSONFormatter{
|
||||
TimestampFormat: c.String("log-timestamp"),
|
||||
})
|
||||
} else {
|
||||
logrus.SetFormatter(&nested.Formatter{
|
||||
FieldsOrder: []string{
|
||||
"version", "url",
|
||||
"config", "file", "mode",
|
||||
"addr", "src", "dst", "session", "action",
|
||||
"error",
|
||||
},
|
||||
TimestampFormat: c.String("log-timestamp"),
|
||||
})
|
||||
}
|
||||
|
||||
if !c.Bool("no-check") {
|
||||
go checkUpdate()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func commonFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "config file",
|
||||
EnvVars: []string{"HYSTERIA_CONFIG"},
|
||||
Value: "./config.json",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Usage: "log level",
|
||||
EnvVars: []string{"HYSTERIA_LOG_LEVEL", "LOGGING_LEVEL"},
|
||||
Value: "debug",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-timestamp",
|
||||
Usage: "log timestamp format",
|
||||
EnvVars: []string{"HYSTERIA_LOG_TIMESTAMP", "LOGGING_TIMESTAMP_FORMAT"},
|
||||
Value: time.RFC3339,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-format",
|
||||
Usage: "log output format",
|
||||
EnvVars: []string{"HYSTERIA_LOG_FORMAT", "LOGGING_FORMATTER"},
|
||||
Value: "txt",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-check",
|
||||
Usage: "disable update check",
|
||||
EnvVars: []string{"HYSTERIA_CHECK_UPDATE"},
|
||||
},
|
||||
}
|
||||
}
|
235
cmd/server.go
235
cmd/server.go
|
@ -1,235 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/lucas-clemente/quic-go/congestion"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tobyxdd/hysteria/pkg/acl"
|
||||
"github.com/tobyxdd/hysteria/pkg/auth"
|
||||
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
|
||||
"github.com/tobyxdd/hysteria/pkg/core"
|
||||
"github.com/tobyxdd/hysteria/pkg/obfs"
|
||||
"github.com/tobyxdd/hysteria/pkg/transport"
|
||||
"github.com/yosuke-furukawa/json5/encoding/json5"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func server(config *serverConfig) {
|
||||
logrus.WithField("config", config.String()).Info("Server configuration loaded")
|
||||
// Resolver
|
||||
if len(config.Resolver) > 0 {
|
||||
setResolver(config.Resolver)
|
||||
}
|
||||
// Load TLS config
|
||||
var tlsConfig *tls.Config
|
||||
if len(config.ACME.Domains) > 0 {
|
||||
// ACME mode
|
||||
tc, err := acmeTLSConfig(config.ACME.Domains, config.ACME.Email,
|
||||
config.ACME.DisableHTTPChallenge, config.ACME.DisableTLSALPNChallenge,
|
||||
config.ACME.AltHTTPPort, config.ACME.AltTLSALPNPort)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
}).Fatal("Failed to get a certificate with ACME")
|
||||
}
|
||||
tc.MinVersion = tls.VersionTLS13
|
||||
tlsConfig = tc
|
||||
} else {
|
||||
// Local cert mode
|
||||
kpl, err := newKeypairLoader(config.CertFile, config.KeyFile)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"cert": config.CertFile,
|
||||
"key": config.KeyFile,
|
||||
}).Fatal("Failed to load the certificate")
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
GetCertificate: kpl.GetCertificateFunc(),
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
}
|
||||
if config.ALPN != "" {
|
||||
tlsConfig.NextProtos = []string{config.ALPN}
|
||||
} else {
|
||||
tlsConfig.NextProtos = []string{DefaultALPN}
|
||||
}
|
||||
// QUIC config
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: config.ReceiveWindowConn,
|
||||
MaxStreamReceiveWindow: config.ReceiveWindowConn,
|
||||
InitialConnectionReceiveWindow: config.ReceiveWindowClient,
|
||||
MaxConnectionReceiveWindow: config.ReceiveWindowClient,
|
||||
MaxIncomingStreams: int64(config.MaxConnClient),
|
||||
KeepAlive: true,
|
||||
DisablePathMTUDiscovery: config.DisableMTUDiscovery,
|
||||
EnableDatagrams: true,
|
||||
}
|
||||
if config.ReceiveWindowConn == 0 {
|
||||
quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow
|
||||
quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow
|
||||
}
|
||||
if config.ReceiveWindowClient == 0 {
|
||||
quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow
|
||||
quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow
|
||||
}
|
||||
if quicConfig.MaxIncomingStreams == 0 {
|
||||
quicConfig.MaxIncomingStreams = DefaultMaxIncomingStreams
|
||||
}
|
||||
// Auth
|
||||
var authFunc func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string)
|
||||
var err error
|
||||
switch authMode := config.Auth.Mode; authMode {
|
||||
case "", "none":
|
||||
logrus.Warn("No authentication configured")
|
||||
authFunc = func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
|
||||
return true, "Welcome"
|
||||
}
|
||||
case "password":
|
||||
logrus.Info("Password authentication enabled")
|
||||
var pwdConfig map[string]string
|
||||
err = json5.Unmarshal(config.Auth.Config, &pwdConfig)
|
||||
if err != nil || len(pwdConfig["password"]) == 0 {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
}).Fatal("Invalid password authentication config")
|
||||
}
|
||||
pwd := pwdConfig["password"]
|
||||
authFunc = func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
|
||||
if string(auth) == pwd {
|
||||
return true, "Welcome"
|
||||
} else {
|
||||
return false, "Wrong password"
|
||||
}
|
||||
}
|
||||
case "external":
|
||||
logrus.Info("External authentication enabled")
|
||||
var extConfig map[string]string
|
||||
err = json5.Unmarshal(config.Auth.Config, &extConfig)
|
||||
if err != nil || len(extConfig["http"]) == 0 {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
}).Fatal("Invalid external authentication config")
|
||||
}
|
||||
provider := &auth.HTTPAuthProvider{
|
||||
Client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
URL: extConfig["http"],
|
||||
}
|
||||
authFunc = provider.Auth
|
||||
default:
|
||||
logrus.WithField("mode", config.Auth.Mode).Fatal("Unsupported authentication mode")
|
||||
}
|
||||
// Obfuscator
|
||||
var obfuscator core.Obfuscator
|
||||
if len(config.Obfs) > 0 {
|
||||
obfuscator = obfs.NewXPlusObfuscator([]byte(config.Obfs))
|
||||
}
|
||||
// IPv6 only mode
|
||||
if config.IPv6Only {
|
||||
transport.DefaultTransport = transport.IPv6OnlyTransport
|
||||
}
|
||||
// ACL
|
||||
var aclEngine *acl.Engine
|
||||
if len(config.ACL) > 0 {
|
||||
aclEngine, err = acl.LoadFromFile(config.ACL, transport.DefaultTransport)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"file": config.ACL,
|
||||
}).Fatal("Failed to parse ACL")
|
||||
}
|
||||
aclEngine.DefaultAction = acl.ActionDirect
|
||||
}
|
||||
// Server
|
||||
var promReg *prometheus.Registry
|
||||
if len(config.PrometheusListen) > 0 {
|
||||
promReg = prometheus.NewRegistry()
|
||||
go func() {
|
||||
http.Handle("/metrics", promhttp.HandlerFor(promReg, promhttp.HandlerOpts{}))
|
||||
err := http.ListenAndServe(config.PrometheusListen, nil)
|
||||
logrus.WithField("error", err).Fatal("Prometheus HTTP server error")
|
||||
}()
|
||||
}
|
||||
server, err := core.NewServer(config.Listen, config.Protocol, tlsConfig, quicConfig, transport.DefaultTransport,
|
||||
uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
|
||||
func(refBPS uint64) congestion.CongestionControl {
|
||||
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
|
||||
}, config.DisableUDP, aclEngine, obfuscator, authFunc,
|
||||
tcpRequestFunc, tcpErrorFunc, udpRequestFunc, udpErrorFunc, promReg)
|
||||
if err != nil {
|
||||
logrus.WithField("error", err).Fatal("Failed to initialize server")
|
||||
}
|
||||
defer server.Close()
|
||||
logrus.WithField("addr", config.Listen).Info("Server up and running")
|
||||
|
||||
err = server.Serve()
|
||||
logrus.WithField("error", err).Fatal("Server shutdown")
|
||||
}
|
||||
|
||||
func tcpRequestFunc(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
"action": actionToString(action, arg),
|
||||
}).Debug("TCP request")
|
||||
}
|
||||
|
||||
func tcpErrorFunc(addr net.Addr, auth []byte, reqAddr string, err error) {
|
||||
if err != io.EOF {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
"error": err,
|
||||
}).Info("TCP error")
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"dst": reqAddr,
|
||||
}).Debug("TCP EOF")
|
||||
}
|
||||
}
|
||||
|
||||
func udpRequestFunc(addr net.Addr, auth []byte, sessionID uint32) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"session": sessionID,
|
||||
}).Debug("UDP request")
|
||||
}
|
||||
|
||||
func udpErrorFunc(addr net.Addr, auth []byte, sessionID uint32, err error) {
|
||||
if err != io.EOF {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"session": sessionID,
|
||||
"error": err,
|
||||
}).Info("UDP error")
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"src": addr.String(),
|
||||
"session": sessionID,
|
||||
}).Debug("UDP EOF")
|
||||
}
|
||||
}
|
||||
|
||||
func actionToString(action acl.Action, arg string) string {
|
||||
switch action {
|
||||
case acl.ActionDirect:
|
||||
return "Direct"
|
||||
case acl.ActionProxy:
|
||||
return "Proxy"
|
||||
case acl.ActionBlock:
|
||||
return "Block"
|
||||
case acl.ActionHijack:
|
||||
return "Hijack to " + arg
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const githubAPIURL = "https://api.github.com/repos/HyNetwork/hysteria/releases/latest"
|
||||
|
||||
type releaseInfo struct {
|
||||
URL string `json:"html_url"`
|
||||
TagName string `json:"tag_name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
}
|
||||
|
||||
func checkUpdate() {
|
||||
info, err := fetchLatestRelease()
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
}).Warn("Failed to check for updates")
|
||||
} else if info.TagName != appVersion {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"version": info.TagName,
|
||||
"url": info.URL,
|
||||
}).Info("New version available")
|
||||
}
|
||||
}
|
||||
|
||||
func fetchLatestRelease() (*releaseInfo, error) {
|
||||
hc := &http.Client{
|
||||
Timeout: time.Second * 20,
|
||||
}
|
||||
resp, err := hc.Get(githubAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var info releaseInfo
|
||||
err = json.Unmarshal(body, &info)
|
||||
return &info, err
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue